From 6e2bed1f3e945659530b0da8b7330afd6f605b3d Mon Sep 17 00:00:00 2001 From: Alexey Khivin Date: Mon, 14 Mar 2016 17:05:01 +0300 Subject: [PATCH] [OpenLDAP] Puppet based version The new version of OpenLDAP application which has the same functionality but uses puppet module to deploy OpenLDAP https://forge.puppetlabs.com/datacentred/ldap Change-Id: I04328dbf596487bdd16a0db24d3b5268c658dfd9 --- .../OpenLDAP2/package/Classes/OpenLDAP.yaml | 111 ++++++++++++++++++ .../Resources/ConfigureOpenLDAPUser.template | 21 ++++ .../package/Resources/DeployOpenLDAP.template | 18 +++ .../scripts/configureOpenLDAPUser.sh | 34 ++++++ .../Resources/scripts/deployOpenLDAP.sh | 13 ++ .../package/Resources/scripts/site.pp | 43 +++++++ murano-apps/OpenLDAP2/package/UI/ui.yaml | 104 ++++++++++++++++ murano-apps/OpenLDAP2/package/logo.png | Bin 0 -> 9394 bytes murano-apps/OpenLDAP2/package/manifest.yaml | 12 ++ 9 files changed, 356 insertions(+) create mode 100644 murano-apps/OpenLDAP2/package/Classes/OpenLDAP.yaml create mode 100644 murano-apps/OpenLDAP2/package/Resources/ConfigureOpenLDAPUser.template create mode 100644 murano-apps/OpenLDAP2/package/Resources/DeployOpenLDAP.template create mode 100644 murano-apps/OpenLDAP2/package/Resources/scripts/configureOpenLDAPUser.sh create mode 100644 murano-apps/OpenLDAP2/package/Resources/scripts/deployOpenLDAP.sh create mode 100644 murano-apps/OpenLDAP2/package/Resources/scripts/site.pp create mode 100644 murano-apps/OpenLDAP2/package/UI/ui.yaml create mode 100644 murano-apps/OpenLDAP2/package/logo.png create mode 100644 murano-apps/OpenLDAP2/package/manifest.yaml diff --git a/murano-apps/OpenLDAP2/package/Classes/OpenLDAP.yaml b/murano-apps/OpenLDAP2/package/Classes/OpenLDAP.yaml new file mode 100644 index 0000000..47a2d1b --- /dev/null +++ b/murano-apps/OpenLDAP2/package/Classes/OpenLDAP.yaml @@ -0,0 +1,111 @@ +Namespaces: + =: io.murano.opaas + std: io.murano + res: io.murano.resources + sys: io.murano.system + puppet: io.murano.opaas.puppet + + +Name: OpenLDAP + +Extends: std:Application + +Properties: + instance: + Contract: $.class(puppet:PuppetInstance).notNull() + name: + Contract: $.string().notNull() + domain: + Contract: $.string() + ldapUser: + Contract: $.string() + ldapPass: + Contract: $.string() + +Methods: + initialize: + Body: + - $._environment: $.find(std:Environment).require() + + deploy: + Body: + - If: not $.getAttr(deployed, false) + Then: + - $securityGroupIngress: + - ToPort: 389 + FromPort: 389 + IpProtocol: tcp + External: true + - ToPort: 636 + FromPort: 636 + IpProtocol: tcp + External: true + - $._environment.securityGroupManager.addGroupIngress($securityGroupIngress) + - $._environment.reporter.report($this, 'Creating VM for OpenLDAP {0}'.format($.instance.openstackId)) + - $.instance.deploy() + - $._environment.reporter.report($this, 'VM Created {0}'.format($.instance.openstackId)) + + - $.instance.installPuppetModule('datacentred-ldap') + + - $.createConfiguration() + + + - $resources: new(sys:Resources) + - $template: $resources.yaml('DeployOpenLDAP.template') + - $._environment.reporter.report($this, 'OpenLDAP deploying') + - $.instance.agent.call($template, $resources) + - $._environment.reporter.report($this, format('OpenLDAP is available at {0}', $.instance.floatingIpAddress)) + + - If: $.domain != '' and $.domain != null + Then: + - If: $.ldapUser != '' and $.ldapUser != null + Then: + - $.configureOpenLDAPUser($.domain, $.ldapUser, $.ldapPass) + - $.setAttr(deployed, true) + - $._environment.reporter.report($this, 'OpenLDAP is deployed!') + + createConfiguration: + Body: + - $.instance.setHieraValue('ldap_domain', $.domain) + - $.instance.setHieraValue('ldap_dc', $.domain.split('.')[0]) + - $.instance.setHieraValue('ldap_user', $.ldapUser) + - $.instance.setHieraValue('ldap_password', $.ldapPass) + + - !yaql "$.instance.setHieraValue('ldap::client::uri', 'localhost')" + - !yaql "$.instance.setHieraValue('ldap::server::rootpw', $.ldapPass)" + - !yaql "$.instance.setHieraValue('ldap::client::ssl_cert', '')" + + # + # these values are also supported by puppet module + # + #- !yaql "$.instance.setHieraValue('ldap::client::base', 'dc=example,dc=com')" + #- !yaql "$.instance.setHieraValue('ldap::client::ssl', 'false')" + #- !yaql "$.instance.setHieraValue('ldap::server::suffix', 'dc=example,dc=com')" + #- !yaql "$.instance.setHieraValue('ldap::server::rootdn', 'cn=admin,dc=example,dc=com')" + #- !yaql "$.instance.setHieraValue('ldap::server::ssl', false)" + #- !yaql "$.instance.setHieraValue('ldap::server::ssl_cacert', '')" + #- !yaql "$.instance.setHieraValue('ldap::server::ssl_cert', '')" + #- !yaql "$.instance.setHieraValue('ldap::server::ssl_key', '')" + + configureOpenLDAPUser: + Arguments: + - domain: + Contract: $.string().notNull() + - ldapUser: + Contract: $.string().notNull() + - ldapPass: + Contract: $.string().notNull() + Body: + - $resources: new(sys:Resources) + - $template: $resources.yaml('ConfigureOpenLDAPUser.template').bind(dict( + domain => $domain, + ldapUser => $ldapUser, + ldapPass => $ldapPass + )) + - $.instance.agent.call($template, $resources) + - $._environment.reporter.report($this, 'OpenLDAP user {0} is added'.format($ldapUser)) + + destroy: + Body: + - $.reportDestroyed() + - $.setAttr(deployed, false) diff --git a/murano-apps/OpenLDAP2/package/Resources/ConfigureOpenLDAPUser.template b/murano-apps/OpenLDAP2/package/Resources/ConfigureOpenLDAPUser.template new file mode 100644 index 0000000..b91e88e --- /dev/null +++ b/murano-apps/OpenLDAP2/package/Resources/ConfigureOpenLDAPUser.template @@ -0,0 +1,21 @@ +FormatVersion: 2.0.0 +Version: 1.0.0 +Name: Configure OpenLDAP + +Parameters: + domain: $domain + ldapUser: $ldapUser + ldapPass: $ldapPass + +Body: | + return configure('{0} {1} {2}'.format(args.domain, args.ldapUser, args.ldapPass)).stdout + +Scripts: + configure: + Type: Application + Version: 1.0.0 + EntryPoint: configureOpenLDAPUser.sh + Files: [] + Options: + captureStdout: true + captureStderr: true diff --git a/murano-apps/OpenLDAP2/package/Resources/DeployOpenLDAP.template b/murano-apps/OpenLDAP2/package/Resources/DeployOpenLDAP.template new file mode 100644 index 0000000..ffd48a9 --- /dev/null +++ b/murano-apps/OpenLDAP2/package/Resources/DeployOpenLDAP.template @@ -0,0 +1,18 @@ +FormatVersion: 2.1.0 +Version: 1.0.0 +Name: Deploy OpenLDAP + +Parameters: + +Body: | + return deploy().stdout + +Scripts: + deploy: + Type: Application + Version: 1.0.0 + EntryPoint: deployOpenLDAP.sh + Files: ['site.pp'] + Options: + captureStdout: true + captureStderr: true diff --git a/murano-apps/OpenLDAP2/package/Resources/scripts/configureOpenLDAPUser.sh b/murano-apps/OpenLDAP2/package/Resources/scripts/configureOpenLDAPUser.sh new file mode 100644 index 0000000..a8b64ba --- /dev/null +++ b/murano-apps/OpenLDAP2/package/Resources/scripts/configureOpenLDAPUser.sh @@ -0,0 +1,34 @@ +#!/bin/bash +DOMAIN="$1" +USERNAME="$2" +PASSWORD="$3" + +DOMAIN_PASSWORD=$PASSWORD + +NAME="`echo "$DOMAIN" | cut -d. -f1`" +TLD="`echo "$DOMAIN" | cut -d. -f2`" + + +ldapadd -x -w $DOMAIN_PASSWORD -D "cn=${USERNAME},dc=${NAME},dc=${TLD}" << USER +dn: uid=${USERNAME},ou=users,dc=${NAME},dc=${TLD} +objectClass: top +objectClass: account +objectClass: posixAccount +objectClass: shadowAccount +cn: ${USERNAME} +uid: ${USERNAME} +uidNumber: 1001 +gidNumber: 1001 +homeDirectory: /home/${USERNAME} +loginShell: /bin/bash +gecos: ${USERNAME}@${DOMAIN} +userPassword: {crypt}x +shadowLastChange: 0 +shadowMax: 0 +shadowWarning: 0 +USER + +ldappasswd -w $DOMAIN_PASSWORD -s ${PASSWORD} -D "cn=${USERNAME},dc=${NAME},dc=${TLD}" -x uid=${USERNAME},ou=users,dc=${NAME},dc=${TLD} + +# check if user been created +ldapwhoami -x -w ${PASSWORD} -D uid=${USERNAME},ou=users,dc=${NAME},dc=${TLD} -hlocalhost -p389 \ No newline at end of file diff --git a/murano-apps/OpenLDAP2/package/Resources/scripts/deployOpenLDAP.sh b/murano-apps/OpenLDAP2/package/Resources/scripts/deployOpenLDAP.sh new file mode 100644 index 0000000..77e8eeb --- /dev/null +++ b/murano-apps/OpenLDAP2/package/Resources/scripts/deployOpenLDAP.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +DEBIAN_FRONTEND=noninteractive apt-get install -y slapd ldap-utils + +# needed to use ldap puppet module +# https://forge.puppetlabs.com/datacentred/ldap +apt-get install ruby-net-ldap + +puppet apply site.pp + +# Open firewall for ldap/ldaps +iptables -I INPUT 1 -p tcp -m tcp --dport 389 -j ACCEPT -m comment --comment "by murano, OpenLDAP server access on port 389" +iptables -I INPUT 1 -p tcp -m tcp --dport 636 -j ACCEPT -m comment --comment "by murano, OpenLDAP server access on port 636" diff --git a/murano-apps/OpenLDAP2/package/Resources/scripts/site.pp b/murano-apps/OpenLDAP2/package/Resources/scripts/site.pp new file mode 100644 index 0000000..970fa4d --- /dev/null +++ b/murano-apps/OpenLDAP2/package/Resources/scripts/site.pp @@ -0,0 +1,43 @@ +node default { + + $dc = hiera("ldap_dc") + $dn = domain2dn(hiera("ldap_domain")) + $user = hiera('ldap_user') + + class { 'ldap::server': + suffix => $dn, + rootdn => "cn=$user,$dn", + rootpw => hiera('ldap_password'), + } + + $ldap_defaults = { + ensure => present, + base => $dn, + host => 'localhost', + port => 389, + ssl => false, + username => "cn=$user,${dn}", + password => hiera('ldap_password') + } + + $ldap_entries = { + "$dn" =>{ + attributes => { + dc => "$dc", + objectClass => ['top','domain'], + description => 'Tree root' + }, + }, + "ou=users,$dn" =>{ + attributes => { + ou => "users", + objectClass=>['top', 'organizationalUnit'], + description=> "Users for ${dn}", + } + }, + } + + create_resources('ldap_entry', $ldap_entries,$ldap_defaults) +} + + diff --git a/murano-apps/OpenLDAP2/package/UI/ui.yaml b/murano-apps/OpenLDAP2/package/UI/ui.yaml new file mode 100644 index 0000000..04bbb5a --- /dev/null +++ b/murano-apps/OpenLDAP2/package/UI/ui.yaml @@ -0,0 +1,104 @@ +Version: 2 + +Application: + ?: + type: io.murano.opaas.OpenLDAP + name: $.appConfiguration.name + domain: $.appConfiguration.domain + ldapUser: $.appConfiguration.ldapUser + ldapPass: $.appConfiguration.ldapPass + instance: + ?: + type: io.murano.opaas.puppet.PuppetInstance + name: generateHostname($.instanceConfiguration.unitNamingPattern, 1) + flavor: $.instanceConfiguration.flavor + image: $.instanceConfiguration.osImage + keyname: $.instanceConfiguration.keyPair + availabilityZone: $.instanceConfiguration.availabilityZone + assignFloatingIp: $.appConfiguration.assignFloatingIP + +Forms: + - appConfiguration: + fields: + - name: name + type: string + label: Application Name + initial: OpenLDAP + description: >- + Enter a desired name for the application. Just A-Z, a-z, 0-9, dash + and underline are allowed + - name: domain + type: string + label: Domain + initial: domain.tld + required: false + descriptionTitle: Domain + description: >- + Please, provide domain for the OpenLDAP instance + - name: ldapUser + type: string + label: Username + required: false + descriptionTitle: LDAP User + description: >- + Please, provide username + - name: ldapPass + type: password + label: Password + required: false + descriptionTitle: LDAP Password + description: >- + Please, provide password + - name: assignFloatingIP + type: boolean + label: Assign Floating IP + description: >- + Select to true to assign floating IP automatically + initial: true + required: false + - instanceConfiguration: + fields: + - name: title + type: string + required: false + hidden: true + description: Specify some instance parameters on which the application would be created + - name: flavor + type: flavor + label: Instance flavor + description: >- + Select registered in Openstack flavor. Consider that application performance + depends on this parameter. + initial: m1.tiny + required: false + - name: osImage + type: image + imageType: linux + label: Instance image + description: >- + Select a valid image for the application. Image should already be prepared and + registered in glance. + - name: keyPair + type: keypair + label: Key Pair + description: >- + Select a Key Pair to control access to instances. You can login to + instances using this KeyPair after the deployment of application. + required: false + - name: availabilityZone + type: azone + label: Availability zone + description: Select availability zone where the application would be installed. + required: false + - name: unitNamingPattern + type: string + label: Instance Naming Pattern + required: false + maxLength: 200 + regexpValidator: '^[-_\w]+$' + errorMessages: + invalid: Just letters, numbers, underscores and hyphens are allowed. + helpText: Just letters, numbers, underscores and hyphens are allowed. + description: >- + Specify a string, that will be used in instance hostname. + Just A-Z, a-z, 0-9, dash and underline are allowed. diff --git a/murano-apps/OpenLDAP2/package/logo.png b/murano-apps/OpenLDAP2/package/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7d8e7dbaeaed6ee02df87a5ebfe478fd0af3dfd8 GIT binary patch literal 9394 zcmV;jBu(3iP)tGeASx7sarw`B(>z9%WGrM|cN-mZJ<{QqBnRdIw6 z*sx*a>H}|o1vYHhc0~5LE{{DWuNIG($XlJYT1z+b4Q z`b#5b(!XJ`H{ku6)9K{r&YgRTzW1d4)!VS~29T4HvbVeo)xkRWOy55{T+WA-aQwQx zv3<{bXNyt)(TZ-j=IJ3sujWkdC{Ju_+J=oQPR^t;Owj8;j~?$i?}GR&REMU(C`m|H?(==qQ%1e>5XY+PY-YNiRlr+FF$kXZId=G>}pu3=_V9IgXz{`2n3?eBXCz_|AmOpobXdRz53 zh(^4p4urpO`NB~)Y`lfZ$rUJLETc$*Sa;Vur#&-mX<$x#lLD?=1d)=p@dAh5Gf^BL zI0xc4(Qu{`hG8Ibqyox84=S(YFz-6y-$OxlSMZGTdq{R8*sw7!IlY3jKp)fKm%FEb zXGU4soTwRtP{Kp#qSXkLm{c)iaTO;IMe)$g$5Fyn;z!?l7H4L@i1*K1g;Tp#eDJ+@ z;dIqLv7_^`PX)V`&9qO@?PjoHV;pk2xcm}k5EFHB;N5dwwfCw$ad1Uyax;XXDyY z&PjY}ekKF+t7ZREO)$ZSc$l=1a;ltV2BO{sVqpzhj{&d8gI@j>d~*8(nB<;@sB{hu zQv;Bkl)xkifvU8j0Zy0bYn)LsmE(#drwtp|6gj5|W#b-5=zlU1=g6GsC6t^j3M;(i zgt;gIy;g@3){%(oU_u;?sc^_Hl+EIybH}JZD}y&g44*ck#{|SFfvXk^P)#Fp=0x~3 z3nJODVPgz(Zi&A>4l!xb2&Pn)l+P@y0Ac>6WP%$KAZ~ZToUZ`YN>W`}o;&>~q3Rgy z18Qo0Fid(Zb!mHF($V#&I0${;GnL2gLp?t|-x1ksM{I4_7>ArD#+h_&0NzUZZH>1E z)+kdV5W6KxR_bmJ3cw%_@r(>dwTOgZz|e_BP@d-y3MQzD>wwoU)4vf+xGJ*{o0#oO}g?TF# z&*c(f@`ReS8g$)+Nb9R_3?NL&7>%lsBpxhTq2WOZ<>f9M*wOcj8Z~!19ky)RurV$< zWlqR>fC?Q8U6UT0y1%xux3V8mhXSwWfMJ-H;*o%vw0lhsUZ2c@JeGQjB2`pMNJd(P z`l9S_9-lye7jWwJu=>*GGix-%h!`y0$c7CYW0P~9P?z%notR}!j*2_gunEt8FNPPz zp(J$34#5f%S=>rl#4H$Oky1>)HmSNY0yDz0AsddGy43x|(S!Y8(iCG0d(X7jFtTA| zd~#0V%f~Wn%bc84WjajoJt6}0+z3XZsN%ig;yj3pSYw@OSX|?hfg!^nFcMT7sX5Dh zQ07{0f1&&PM|TZ;)(!&Muu&j61G+2c0hDZE0bG#q)PSX`=P)Fcbp)Vy@^CmM7|Y`z zrSuKve{w0sOp#Jo49ho_Wgd~RYVLfwXYI*Dq0iD!wdK-=jY7$J-Wbk#fI-aO3eB^> z!tHLf6ta(p`?L&AH($#(&EKz-7en2!KkLBjOq}(M3@2kx>~nt(c=cR zs0QBTAsD=s1TP32fKH;qP~C|A4dv zC6Wtvgw}?QBFMRnpO(wtgv<-Gtf7%60&R(c;T{ccp9J?pN-&k9=IaGBm5CuECSkRT z#G0hSJ2_1D_-EX8eh#FBfMFp4J6+F)jbeOG^J1Tq4Rd&jqqjtK!sz8ua@GsgM-_A% zebX<3yN;T)Dpg6rguxjg)F
    {TYDY<_3!{H6Eby=>v4uojngZ#((bU|*zdtfU7U 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