Merge lp:~jose/charms/precise/owncloud/port-change+repo+ssl-support into lp:charms/owncloud

Proposed by José Antonio Rey
Status: Merged
Merged at revision: 26
Proposed branch: lp:~jose/charms/precise/owncloud/port-change+repo+ssl-support
Merge into: lp:charms/owncloud
Diff against target: 2050 lines (+1592/-251)
18 files modified
README.md (+110/-33)
charm-helpers.yaml (+5/-0)
config.yaml (+28/-0)
hooks/charmhelpers/contrib/ssl/__init__.py (+78/-0)
hooks/charmhelpers/contrib/ssl/service.py (+267/-0)
hooks/charmhelpers/core/hookenv.py (+401/-0)
hooks/charmhelpers/core/host.py (+297/-0)
hooks/config-changed (+153/-9)
hooks/db-relation-changed (+9/-0)
hooks/db-relation-departed (+13/-0)
hooks/install (+45/-23)
hooks/ssl (+19/-0)
hooks/start (+6/-2)
hooks/upgrade-charm (+43/-39)
hooks/website-relation-joined (+1/-2)
metadata.yaml (+1/-1)
tests/100-deploy.py (+116/-0)
tests/100_deploy.test (+0/-142)
To merge this branch: bzr merge lp:~jose/charms/precise/owncloud/port-change+repo+ssl-support
Reviewer Review Type Date Requested Status
Charles Butler (community) Approve
Matt Bruzek (community) Needs Fixing
Review via email: mp+215527@code.launchpad.net

Commit message

Added port changing and repository install support, as well as SSL support by default.

Description of the change

These are fixes to bug #1161894, bug #1306756, bug #1310164 and bug #1315091.

Support has been added considering that it will not break previous installs of ownCloud: if a person has an older version and upgrade-charm's the instance(s) will not break.

To post a comment you must log in.
Revision history for this message
Matt Bruzek (mbruzek) wrote :
Download full text (3.2 KiB)

José,

Thanks for working on adding ssh to the Owncloud charm! This is my first experience with Owncloud and I am really excited about this charm!

The command “charm proof” passes with no errors! That is excellent, but I have come to understand that you pay attention to details like that.

I see you added charmhelpers to generate the selfsigned certificate. That all looks good to me.

The README.md file was updated, it looks very good but I found a few small errors:

“If you do not know what it is, execute `juju status` to find out the public DNS.”

The command “juju status” does not return a Domain Name Server (DNS). What you might have meant “... to find out the public domain name of the deployed unit.”

“This password will be used in case you want to deo a setup of Shared Instances. If not provided, it will be randomly generated when a DB relation is joined.”
The word “do” is misspelled here.

The readme file asserts that the charm will not be set up without setting the domain configuration option, which is fine. I tested the server address before setting domain and got the default page:

  It works!
  This is the default web page for this server.
  The web server software is running but no content has been added, yet.

I understand this is the default apache page has not been disabled at this point. What would be really awesome if the default page could provide the user with instructions on what needs to be done. The hook could write a small index.html to the www root that tells the user what is needed before the charm will set up and configure. Something like:

  The owncloud service is not fully configured. Set the domain value in Juju to configure owncloud.

Bugs found unrelated to this merge proposal:

The admin user name and password are immutable, even after adding a mysql database. I realize this is not a bug introduced by this merge proposal so I created a bug here:
https://bugs.launchpad.net/charms/+source/owncloud/+bug/1315047

We can track that topic separately on that bug report.

Removing the database relation generates the following error:

[1044] SQLSTATE[42000] [1044] Access denied for user 'paethaicieniaju'@'%' to database 'owncloud'

I opened a bug here:
https://bugs.launchpad.net/charms/+source/owncloud/+bug/1315091

I am really excited to see that the charm included tests! That is outstanding not many merge propoals have tests. However the test that I ran did not work.

$ ./tests/100_deploy.test
Traceback (most recent call last):
  File "./tests/100_deploy.test", line 25, in <module>
    domain = d.sentry.unit['haproxy/0'].info['public-address']
AttributeError: 'NoneType' object has no attribute 'unit'

I took a look at the test and it looks like an error in the amulet test code. The sentry objects are not created until after d.setup(...). You should move that domain and d.configure() to after the d.setup(...) call.

Thank you so much for your submission! Based on this test failure I have moved this bug to Incomplete. Once you have addressed the issues above either with code changes or comments, and wish to have another review, simply move the bug back to either "New" or "Fix Committed" to have it ad...

Read more...

Revision history for this message
Matt Bruzek (mbruzek) wrote :

The 100_deploy.test fails.

review: Needs Fixing
Revision history for this message
José Antonio Rey (jose) wrote :

As revision 24 was merged in, tests no longer work due to many different factors. There is currently an open bug for it as they need to be updated.

Revision history for this message
Ted Gould (ted) wrote :

Not sure what is blocking this MR from the comments. I'd really like to use these features in the owncloud charm, any chance it could get included? Thanks!

Revision history for this message
José Antonio Rey (jose) wrote :

Ted,

Tests are blocking this MP from being approved. I'll be talking to the person in charge of them to see what can I do to speed up the process. Rest assured that this will be in the Charm Store soon.

Revision history for this message
Matt Bruzek (mbruzek) wrote :

José,

Thank you for following up with the Owncloud charm. Your fixes since my last review have noted and for the most part are great! Keep up the excellent work.

I took some time to review this Merge Proposal again today. Here is what I found.

The Amulet tests still do not pass but since the tests did not pass before this MP that is OK. The tests should pass eventually, so I created a bug to track that work here: https://bugs.launchpad.net/charms/+source/owncloud/+bug/1320324

I am still concerned about the immutable configuration options user and password that was not changed by this MP so the bug can be found here: https://bugs.launchpad.net/charms/+source/owncloud/+bug/1315047

hooks/website-relation-joined

There is an invalid bash variable in there $80. I assume you meant to always set the port to 80. Since the port is a configuration option I think you want to call config-get port to get the port and set it properly for the website relation.

Based on the error in the hook I can not approve this change at this time. Hopefully it is an easy fix and we can move to the next step quickly, as I am genuinely interested in getting owncloud in the charm store!

Thank you so much for the work here, and being patient with the review process. I have moved this proposal to incomplete as a result of the review. Once you have addressed the issue and want another review please set this proposal back to “needs review” and it will be added to the review queue.

review: Needs Fixing
Revision history for this message
Matt Bruzek (mbruzek) wrote :

The final paragraph of my last statement was incorrect. My apologies on this.

Once the problem has been addressed, lick on the “Request another review” link on this merge proposal. That way it will be added to the review queue properly.

If you have any questions/comments/concerns about the review contact us
in #juju on irc.freenode.net or email the mailing list
<email address hidden>

Revision history for this message
Matt Bruzek (mbruzek) wrote :

I took some time to review the latest submission. The amulet test failed with the error message “Unable to validate NFS config 10.0.3.163” on my local system. The tests need to run without errors before we can accept the merge proposal.

The upgrade-charm code needs some improvements. Please use https:// when ever possible with wget, that verifies the identity of the server that you are downloading binaries from. Please change or add https to all wget statements where possible.

The while loop in updgrade-charm has a couple of issues that we would like to see fixed. If you are hardcoding the version of owncloud, you should hardcode the md5sum in the charm code, this prevents Man In the Middle attacks from faking a payload and generating an md5sum with the right hash value of the payload. Also it is possible that the while loop would never exit if the md5 sum was incorrect or if the download was unavailable. The code should fail if wget fails to download the tar file, not loop forever.

In the config-changed hook it appears that the if-elif only sets the user OR the password not both. For instance if the user equals the contents of .admin the password code can not be executed. Please revisit this code and fix the either or situation.

Thanks again for the submission. I am going to put this MP to "needs fixing" and when you are ready for another review please click the "Request another review" button in the upper right hand corner of the commit message.

review: Needs Fixing
38. By José Antonio Rey

Fixed various bugs

Revision history for this message
José Antonio Rey (jose) wrote :

Matt,

All the issues have now been fixed, except the NFS one. This is due to NFS failing on the local provider, it's not test-specific.

On the other hand, I added a section on the README specifying to which websites and ports the charm needs to connect in order to successfully deploy, as this is not a fat charm.

Revision history for this message
Matt Bruzek (mbruzek) wrote :

José,

Thank you for your submission for owncloud! Your continued dedication to the owncloud charm is very much appreciated.

I took some time to review the latest submission. The amulet test failed on hp-cloud with the error message:

HTTPSConnectionPool(host='15.125.126.129', port=443): Max retries exceeded with url: /index.php

We spoke about this and I understand this is because the 'domain' configuration value is not set after the deployment. We should try to set that and re-run the tests.

The tests/00_setup.sh tests need the executable flag.

Thank you for fixing the bugs I pointed out in the previous review. Also I really like the way you wrote the README.md with the Internet Connection Requirements section! Very well done sir, I think this will be a model for future charm README files.

config-changed:

I noticed something very strange in the config-changed hook:
sed -i 's/;upload_max_filesize/;upload_max_filesize/g' /etc/php5/apache2/php.ini

This seems to be searching for a term and replacing the exact same term. I believe this to be a mistake and it should be searching for '^upload_max_filesize” and replacing with the commented out version.

I realize this is not in your MP but please fix this problem.

Thanks for your time and work on this owncloud submission! I am going to put this MP to "needs fixing" and when you are ready for another review please click the "Request another review" button in the upper right hand corner of the commit message.

review: Needs Fixing
39. By José Antonio Rey

Fixed default behaviour when domain wasn't set and made setup test +x

Revision history for this message
José Antonio Rey (jose) wrote :

Hey Matt,

All of this has been fixed as of the latest revision of the branch.

40. By José Antonio Rey

Updated the author's email address

41. By José Antonio Rey

Updated Release key sha1sum, updated source to version 6.0.4

42. By José Antonio Rey

Updated README, fixed idempotency on src option

Revision history for this message
Charles Butler (lazypower) wrote :

hey Jose,

Thanks for the continued effort on getting this merge prepared for the charm store.

I've had a chance to review, and per our feedback in IRC i see that you've added configuration options when using the deploy from source option. This will help aleviate additional stress on you as the charm maintainer when users want to run from more recent copies upstream, and the deployment routine hasn't changed.

I've taken some time to grease the wheels and deploy owncloud in it's various configurations. Ideally I'd like to see more tests against this charm with those various deployment models, such as generating SSH keys post deployment, deploying with SSH keys as a config option, and switching up the deployment model from DEBS to SOURCE, and back post deployment.

Otherwise I've run several deployments and they all came back good for me on my MAAS host and AWS.

I'm going to approve this, but caution you to break up future merges into smaller digestable chunks as this was a fairly large review that ultimately delayed the merge from happening as quickly as we would like.

Thanks again for such dedication and great work.

+1 LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'README.md'
--- README.md 2014-04-11 13:44:05 +0000
+++ README.md 2014-07-21 22:01:08 +0000
@@ -1,7 +1,7 @@
1# OwnCloud1# OwnCloud
22
3- Author: Atul Jha <atul.jha@csscorp.com>3- Author: Atul Jha <koolhead17@gmail.com>
4- Maintainer: Nathan Williams <nathan@nathanewilliams.com>4- Maintainer: José Antonio Rey <jose@ubuntu.com>
55
6# Overview6# Overview
77
@@ -14,32 +14,76 @@
14sqlite as a standalone server. Provides relationship hooks with NFS file14sqlite as a standalone server. Provides relationship hooks with NFS file
15storage, MySQL Databases, and HAProxy reverse proxy charms.15storage, MySQL Databases, and HAProxy reverse proxy charms.
1616
17## Preparation:17# Configuration
1818
191. the charm comes with port, user, password configuration options. 19This charm comes with different configuration options. Optional configuration
2020options include:
21 if no user is provided, the administrator will be "owncloud".21
2222`domain`: This is the domain or IP address for your ownCloud instance. If you
23 if no password is provided, a random one is chosen during the23do not know what it is, execute `juju status` to find out the public address. If
24 db-relation-joined hook. if you do this, you can obtain the auth24not provided, the charm will refuse to configure.
25 credentials from the juju logs.25
2626`downloadurl`: This is the download URL that the charm will use in case the src
27# Usage:27option is set to `source`. It defaults to the 6.0.4 URL.
2828
29#### 1. Install29`sha1sum`: This is the SHA1SUM for the `downloadurl` file. It defaults to the
30SHA1SUM for the 6.0.4 file.
31
32`port`: This is the alias port that will be used for the ownCloud instance. It
33will redirect to 443, which is the HTTPS port. It defaults to 80.
34
35`src`: This is the source from which the package will be installed. You can
36choose between `repo`, which will install it from a repository built by
37ownCloud, or `source`, which will download the tarball and extract it. It
38defaults to `repo`.
39
40`customssl`: This charm provides default SSL support for ownCloud. This means
41that if you do not provide a custom SSL key and certificate, a self-signed one
42will be auto-generated for you. If you want to use a custom SSL certificate,
43please set this option to `true`. It defaults to `false`.
44
45`sslkey`: If `customssl` is set to true, this is the SSL key that will be used.
46If not provided and `customssl` is true, the charm will refuse to configure.
47
48`sslcert`: If `customssl` is set to true, this is the SSL cert that will be
49used. If not provided and `customssl` is true, the charm will refuse to
50configure.
51
52`user`: This user will be used in case you want to do a setup of Shared
53Instances. If not provided, it will be defaulted to `ownCloud`.
54
55`password`: This password will be used in case you want to do a setup of
56Shared Instances. If not provided, it will be randomly generated when a DB
57relation is joined.
58
59You can put any of this options in a config.yaml file and specify it at the
60moment of deploying. Otherwise, you can change them by executing:
61
62 juju set owncloud [option]=[value]
63
64# Usage
65
66## Standalone Instance
67
68First, bootstrap your environment:
69
70 juju bootstrap
71
72Then, deploy ownCloud by executing the following command:
30 73
31 juju deploy owncloud74 juju deploy owncloud
3275
33#### 2. Expose76Finally, expose the service:
3477
35 juju expose owncloud78 juju expose owncloud
3679
37#### 3a. Standalone Instance80Access OwnCloud service directly, and complete the setup, providing user
3881credentials and initializing sqlite database.
39 Access OwnCloud service directly, and complete the setup, providing user82
40 credentials and initializing sqlite database.83## Shared Instances
4184
42#### 3b. Shared Instances85If you want to deploy shared instances, execute the following commands after
86doing a Standalone Instance setup:
4387
44 juju deploy mysql88 juju deploy mysql
45 juju add-relation mysql owncloud89 juju add-relation mysql owncloud
@@ -47,15 +91,14 @@
47 juju deploy nfs91 juju deploy nfs
48 juju add-relation nfs owncloud92 juju add-relation nfs owncloud
4993
50#### 4. Access 94We're now done! To find out the address for your ownCloud instance, execute
5195`juju status` and navigate to it.
52http://$owncloud-machine-addr/. To find out the public address of ownCloud,96
53look for it in the output of the `juju status` command.97# Scale out Usage
5498
5599In order to do a scalabe deploy of ownCloud, execute the following commands
56## Scale out Usage100
57101 juju bootstrap
58
59 juju deploy owncloud102 juju deploy owncloud
60 juju deploy mysql103 juju deploy mysql
61 juju deploy haproxy104 juju deploy haproxy
@@ -63,13 +106,47 @@
63 juju add-relation haproxy owncloud106 juju add-relation haproxy owncloud
64 juju add-unit owncloud107 juju add-unit owncloud
65108
66109# Internet Connection Requirements
67## Known Limitations110
111This charm downloads files from the Internet, and requires Internet connectivity
112in order to properly install. The requirements vary for each setup type.
113
114## When installing from the source
115
116When installing from the source packages available for download, this charm will
117connect to the following Internet sites:
118
119 * download.owncloud.org with port 443
120 * The Ubuntu repositories or a private mirror of them
121
122## When installing from the repository
123
124ownCloud offers the option to install from a repository. This is the default
125configuration value for the charm. With this, the charm will connect to the
126following Internet sites:
127
128 * download.opensuse.org with port 80
129 * download.opensuse.org as a repository
130 * The Ubuntu repositories or a private mirror of them
131
132# Known Limitations
68133
69If you have been using a standalone instance and want to migrate to a shared134If you have been using a standalone instance and want to migrate to a shared
70instance, please note that adding the mysql relation will not preserve the file135instance, please note that adding the mysql relation will not preserve the file
71structure in the database. This means that your file listing will not be136structure in the database. This means that your file listing will not be
72available. Make sure to have this in mind when doing the migration.137available. Make sure to have this in mind when doing the migration.
73138
139Also, if you leave the `customssl` option set to false or provide a self-signed
140SSL certificate, ownCloud will throw a WebDAV error after creating the admin
141username and password. Ignore this error as it does not affect the working of
142ownCloud (it is silently fixed), and enter your website again.
143
144If port is different than 80, it looks like the instance throws an SSL
145error when connecting. Recommendation is to set the `port` value to 80 to avoid
146problems.
147
148Finally, on the tests side, the tests will fail on the local provider due to
149NFS not being able to deploy correctly (this is an NFS-related issue).
150
74#TODO151#TODO
75Genericize shared-fs-relation-* for non-nfs shared-fs providers152Genericize shared-fs-relation-* for non-nfs shared-fs providers
76153
=== added file 'charm-helpers.yaml'
--- charm-helpers.yaml 1970-01-01 00:00:00 +0000
+++ charm-helpers.yaml 2014-07-21 22:01:08 +0000
@@ -0,0 +1,5 @@
1destination: hooks/charmhelpers
2branch: lp:charm-helpers
3include:
4 - contrib.ssl
5 - core
06
=== modified file 'config.yaml'
--- config.yaml 2014-01-28 15:41:38 +0000
+++ config.yaml 2014-07-21 22:01:08 +0000
@@ -1,4 +1,16 @@
1options:1options:
2 domain:
3 type: string
4 default: ""
5 description: the domain name for your owncloud server
6 downloadurl:
7 type: string
8 default: "https://download.owncloud.org/community/owncloud-6.0.4.tar.bz2"
9 description: url from which owncloud will be downloaded
10 sha1sum:
11 type: string
12 default: "6e341aeba2cf99416de009c7862bf03d90d1b058"
13 description: the sha1sum for the file in the download link
2 port:14 port:
3 type: int15 type: int
4 default: 8016 default: 80
@@ -11,3 +23,19 @@
11 type: string23 type: string
12 default: ""24 default: ""
13 description: default administrative password25 description: default administrative password
26 src:
27 type: string
28 default: "repo"
29 description: source from where the charm will install the package
30 customssl:
31 type: boolean
32 default: false
33 description: option to set if custom ssl certificates are going to be used
34 sslcert:
35 type: string
36 default: ""
37 description: ssl cert to be used if custom is on
38 sslkey:
39 type: string
40 default: ""
41 description: ssl key to be used if custom is on
1442
=== added directory 'hooks/charmhelpers'
=== added file 'hooks/charmhelpers/__init__.py'
=== added directory 'hooks/charmhelpers/contrib'
=== added file 'hooks/charmhelpers/contrib/__init__.py'
=== added directory 'hooks/charmhelpers/contrib/ssl'
=== added file 'hooks/charmhelpers/contrib/ssl/__init__.py'
--- hooks/charmhelpers/contrib/ssl/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/ssl/__init__.py 2014-07-21 22:01:08 +0000
@@ -0,0 +1,78 @@
1import subprocess
2from charmhelpers.core import hookenv
3
4
5def generate_selfsigned(keyfile, certfile, keysize="1024", config=None, subject=None, cn=None):
6 """Generate selfsigned SSL keypair
7
8 You must provide one of the 3 optional arguments:
9 config, subject or cn
10 If more than one is provided the leftmost will be used
11
12 Arguments:
13 keyfile -- (required) full path to the keyfile to be created
14 certfile -- (required) full path to the certfile to be created
15 keysize -- (optional) SSL key length
16 config -- (optional) openssl configuration file
17 subject -- (optional) dictionary with SSL subject variables
18 cn -- (optional) cerfificate common name
19
20 Required keys in subject dict:
21 cn -- Common name (eq. FQDN)
22
23 Optional keys in subject dict
24 country -- Country Name (2 letter code)
25 state -- State or Province Name (full name)
26 locality -- Locality Name (eg, city)
27 organization -- Organization Name (eg, company)
28 organizational_unit -- Organizational Unit Name (eg, section)
29 email -- Email Address
30 """
31
32 cmd = []
33 if config:
34 cmd = ["/usr/bin/openssl", "req", "-new", "-newkey",
35 "rsa:{}".format(keysize), "-days", "365", "-nodes", "-x509",
36 "-keyout", keyfile,
37 "-out", certfile, "-config", config]
38 elif subject:
39 ssl_subject = ""
40 if "country" in subject:
41 ssl_subject = ssl_subject + "/C={}".format(subject["country"])
42 if "state" in subject:
43 ssl_subject = ssl_subject + "/ST={}".format(subject["state"])
44 if "locality" in subject:
45 ssl_subject = ssl_subject + "/L={}".format(subject["locality"])
46 if "organization" in subject:
47 ssl_subject = ssl_subject + "/O={}".format(subject["organization"])
48 if "organizational_unit" in subject:
49 ssl_subject = ssl_subject + "/OU={}".format(subject["organizational_unit"])
50 if "cn" in subject:
51 ssl_subject = ssl_subject + "/CN={}".format(subject["cn"])
52 else:
53 hookenv.log("When using \"subject\" argument you must "
54 "provide \"cn\" field at very least")
55 return False
56 if "email" in subject:
57 ssl_subject = ssl_subject + "/emailAddress={}".format(subject["email"])
58
59 cmd = ["/usr/bin/openssl", "req", "-new", "-newkey",
60 "rsa:{}".format(keysize), "-days", "365", "-nodes", "-x509",
61 "-keyout", keyfile,
62 "-out", certfile, "-subj", ssl_subject]
63 elif cn:
64 cmd = ["/usr/bin/openssl", "req", "-new", "-newkey",
65 "rsa:{}".format(keysize), "-days", "365", "-nodes", "-x509",
66 "-keyout", keyfile,
67 "-out", certfile, "-subj", "/CN={}".format(cn)]
68
69 if not cmd:
70 hookenv.log("No config, subject or cn provided,"
71 "unable to generate self signed SSL certificates")
72 return False
73 try:
74 subprocess.check_call(cmd)
75 return True
76 except Exception as e:
77 print "Execution of openssl command failed:\n{}".format(e)
78 return False
079
=== added file 'hooks/charmhelpers/contrib/ssl/service.py'
--- hooks/charmhelpers/contrib/ssl/service.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/ssl/service.py 2014-07-21 22:01:08 +0000
@@ -0,0 +1,267 @@
1import logging
2import os
3from os.path import join as path_join
4from os.path import exists
5import subprocess
6
7
8log = logging.getLogger("service_ca")
9
10logging.basicConfig(level=logging.DEBUG)
11
12STD_CERT = "standard"
13
14# Mysql server is fairly picky about cert creation
15# and types, spec its creation separately for now.
16MYSQL_CERT = "mysql"
17
18
19class ServiceCA(object):
20
21 default_expiry = str(365 * 2)
22 default_ca_expiry = str(365 * 6)
23
24 def __init__(self, name, ca_dir, cert_type=STD_CERT):
25 self.name = name
26 self.ca_dir = ca_dir
27 self.cert_type = cert_type
28
29 ###############
30 # Hook Helper API
31 @staticmethod
32 def get_ca(type=STD_CERT):
33 service_name = os.environ['JUJU_UNIT_NAME'].split('/')[0]
34 ca_path = os.path.join(os.environ['CHARM_DIR'], 'ca')
35 ca = ServiceCA(service_name, ca_path, type)
36 ca.init()
37 return ca
38
39 @classmethod
40 def get_service_cert(cls, type=STD_CERT):
41 service_name = os.environ['JUJU_UNIT_NAME'].split('/')[0]
42 ca = cls.get_ca()
43 crt, key = ca.get_or_create_cert(service_name)
44 return crt, key, ca.get_ca_bundle()
45
46 ###############
47
48 def init(self):
49 log.debug("initializing service ca")
50 if not exists(self.ca_dir):
51 self._init_ca_dir(self.ca_dir)
52 self._init_ca()
53
54 @property
55 def ca_key(self):
56 return path_join(self.ca_dir, 'private', 'cacert.key')
57
58 @property
59 def ca_cert(self):
60 return path_join(self.ca_dir, 'cacert.pem')
61
62 @property
63 def ca_conf(self):
64 return path_join(self.ca_dir, 'ca.cnf')
65
66 @property
67 def signing_conf(self):
68 return path_join(self.ca_dir, 'signing.cnf')
69
70 def _init_ca_dir(self, ca_dir):
71 os.mkdir(ca_dir)
72 for i in ['certs', 'crl', 'newcerts', 'private']:
73 sd = path_join(ca_dir, i)
74 if not exists(sd):
75 os.mkdir(sd)
76
77 if not exists(path_join(ca_dir, 'serial')):
78 with open(path_join(ca_dir, 'serial'), 'wb') as fh:
79 fh.write('02\n')
80
81 if not exists(path_join(ca_dir, 'index.txt')):
82 with open(path_join(ca_dir, 'index.txt'), 'wb') as fh:
83 fh.write('')
84
85 def _init_ca(self):
86 """Generate the root ca's cert and key.
87 """
88 if not exists(path_join(self.ca_dir, 'ca.cnf')):
89 with open(path_join(self.ca_dir, 'ca.cnf'), 'wb') as fh:
90 fh.write(
91 CA_CONF_TEMPLATE % (self.get_conf_variables()))
92
93 if not exists(path_join(self.ca_dir, 'signing.cnf')):
94 with open(path_join(self.ca_dir, 'signing.cnf'), 'wb') as fh:
95 fh.write(
96 SIGNING_CONF_TEMPLATE % (self.get_conf_variables()))
97
98 if exists(self.ca_cert) or exists(self.ca_key):
99 raise RuntimeError("Initialized called when CA already exists")
100 cmd = ['openssl', 'req', '-config', self.ca_conf,
101 '-x509', '-nodes', '-newkey', 'rsa',
102 '-days', self.default_ca_expiry,
103 '-keyout', self.ca_key, '-out', self.ca_cert,
104 '-outform', 'PEM']
105 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
106 log.debug("CA Init:\n %s", output)
107
108 def get_conf_variables(self):
109 return dict(
110 org_name="juju",
111 org_unit_name="%s service" % self.name,
112 common_name=self.name,
113 ca_dir=self.ca_dir)
114
115 def get_or_create_cert(self, common_name):
116 if common_name in self:
117 return self.get_certificate(common_name)
118 return self.create_certificate(common_name)
119
120 def create_certificate(self, common_name):
121 if common_name in self:
122 return self.get_certificate(common_name)
123 key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name)
124 crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
125 csr_p = path_join(self.ca_dir, "certs", "%s.csr" % common_name)
126 self._create_certificate(common_name, key_p, csr_p, crt_p)
127 return self.get_certificate(common_name)
128
129 def get_certificate(self, common_name):
130 if not common_name in self:
131 raise ValueError("No certificate for %s" % common_name)
132 key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name)
133 crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
134 with open(crt_p) as fh:
135 crt = fh.read()
136 with open(key_p) as fh:
137 key = fh.read()
138 return crt, key
139
140 def __contains__(self, common_name):
141 crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
142 return exists(crt_p)
143
144 def _create_certificate(self, common_name, key_p, csr_p, crt_p):
145 template_vars = self.get_conf_variables()
146 template_vars['common_name'] = common_name
147 subj = '/O=%(org_name)s/OU=%(org_unit_name)s/CN=%(common_name)s' % (
148 template_vars)
149
150 log.debug("CA Create Cert %s", common_name)
151 cmd = ['openssl', 'req', '-sha1', '-newkey', 'rsa:2048',
152 '-nodes', '-days', self.default_expiry,
153 '-keyout', key_p, '-out', csr_p, '-subj', subj]
154 subprocess.check_call(cmd)
155 cmd = ['openssl', 'rsa', '-in', key_p, '-out', key_p]
156 subprocess.check_call(cmd)
157
158 log.debug("CA Sign Cert %s", common_name)
159 if self.cert_type == MYSQL_CERT:
160 cmd = ['openssl', 'x509', '-req',
161 '-in', csr_p, '-days', self.default_expiry,
162 '-CA', self.ca_cert, '-CAkey', self.ca_key,
163 '-set_serial', '01', '-out', crt_p]
164 else:
165 cmd = ['openssl', 'ca', '-config', self.signing_conf,
166 '-extensions', 'req_extensions',
167 '-days', self.default_expiry, '-notext',
168 '-in', csr_p, '-out', crt_p, '-subj', subj, '-batch']
169 log.debug("running %s", " ".join(cmd))
170 subprocess.check_call(cmd)
171
172 def get_ca_bundle(self):
173 with open(self.ca_cert) as fh:
174 return fh.read()
175
176
177CA_CONF_TEMPLATE = """
178[ ca ]
179default_ca = CA_default
180
181[ CA_default ]
182dir = %(ca_dir)s
183policy = policy_match
184database = $dir/index.txt
185serial = $dir/serial
186certs = $dir/certs
187crl_dir = $dir/crl
188new_certs_dir = $dir/newcerts
189certificate = $dir/cacert.pem
190private_key = $dir/private/cacert.key
191RANDFILE = $dir/private/.rand
192default_md = default
193
194[ req ]
195default_bits = 1024
196default_md = sha1
197
198prompt = no
199distinguished_name = ca_distinguished_name
200
201x509_extensions = ca_extensions
202
203[ ca_distinguished_name ]
204organizationName = %(org_name)s
205organizationalUnitName = %(org_unit_name)s Certificate Authority
206
207
208[ policy_match ]
209countryName = optional
210stateOrProvinceName = optional
211organizationName = match
212organizationalUnitName = optional
213commonName = supplied
214
215[ ca_extensions ]
216basicConstraints = critical,CA:true
217subjectKeyIdentifier = hash
218authorityKeyIdentifier = keyid:always, issuer
219keyUsage = cRLSign, keyCertSign
220"""
221
222
223SIGNING_CONF_TEMPLATE = """
224[ ca ]
225default_ca = CA_default
226
227[ CA_default ]
228dir = %(ca_dir)s
229policy = policy_match
230database = $dir/index.txt
231serial = $dir/serial
232certs = $dir/certs
233crl_dir = $dir/crl
234new_certs_dir = $dir/newcerts
235certificate = $dir/cacert.pem
236private_key = $dir/private/cacert.key
237RANDFILE = $dir/private/.rand
238default_md = default
239
240[ req ]
241default_bits = 1024
242default_md = sha1
243
244prompt = no
245distinguished_name = req_distinguished_name
246
247x509_extensions = req_extensions
248
249[ req_distinguished_name ]
250organizationName = %(org_name)s
251organizationalUnitName = %(org_unit_name)s machine resources
252commonName = %(common_name)s
253
254[ policy_match ]
255countryName = optional
256stateOrProvinceName = optional
257organizationName = match
258organizationalUnitName = optional
259commonName = supplied
260
261[ req_extensions ]
262basicConstraints = CA:false
263subjectKeyIdentifier = hash
264authorityKeyIdentifier = keyid:always, issuer
265keyUsage = digitalSignature, keyEncipherment, keyAgreement
266extendedKeyUsage = serverAuth, clientAuth
267"""
0268
=== added directory 'hooks/charmhelpers/core'
=== added file 'hooks/charmhelpers/core/__init__.py'
=== added file 'hooks/charmhelpers/core/hookenv.py'
--- hooks/charmhelpers/core/hookenv.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/hookenv.py 2014-07-21 22:01:08 +0000
@@ -0,0 +1,401 @@
1"Interactions with the Juju environment"
2# Copyright 2013 Canonical Ltd.
3#
4# Authors:
5# Charm Helpers Developers <juju@lists.ubuntu.com>
6
7import os
8import json
9import yaml
10import subprocess
11import sys
12import UserDict
13from subprocess import CalledProcessError
14
15CRITICAL = "CRITICAL"
16ERROR = "ERROR"
17WARNING = "WARNING"
18INFO = "INFO"
19DEBUG = "DEBUG"
20MARKER = object()
21
22cache = {}
23
24
25def cached(func):
26 """Cache return values for multiple executions of func + args
27
28 For example:
29
30 @cached
31 def unit_get(attribute):
32 pass
33
34 unit_get('test')
35
36 will cache the result of unit_get + 'test' for future calls.
37 """
38 def wrapper(*args, **kwargs):
39 global cache
40 key = str((func, args, kwargs))
41 try:
42 return cache[key]
43 except KeyError:
44 res = func(*args, **kwargs)
45 cache[key] = res
46 return res
47 return wrapper
48
49
50def flush(key):
51 """Flushes any entries from function cache where the
52 key is found in the function+args """
53 flush_list = []
54 for item in cache:
55 if key in item:
56 flush_list.append(item)
57 for item in flush_list:
58 del cache[item]
59
60
61def log(message, level=None):
62 """Write a message to the juju log"""
63 command = ['juju-log']
64 if level:
65 command += ['-l', level]
66 command += [message]
67 subprocess.call(command)
68
69
70class Serializable(UserDict.IterableUserDict):
71 """Wrapper, an object that can be serialized to yaml or json"""
72
73 def __init__(self, obj):
74 # wrap the object
75 UserDict.IterableUserDict.__init__(self)
76 self.data = obj
77
78 def __getattr__(self, attr):
79 # See if this object has attribute.
80 if attr in ("json", "yaml", "data"):
81 return self.__dict__[attr]
82 # Check for attribute in wrapped object.
83 got = getattr(self.data, attr, MARKER)
84 if got is not MARKER:
85 return got
86 # Proxy to the wrapped object via dict interface.
87 try:
88 return self.data[attr]
89 except KeyError:
90 raise AttributeError(attr)
91
92 def __getstate__(self):
93 # Pickle as a standard dictionary.
94 return self.data
95
96 def __setstate__(self, state):
97 # Unpickle into our wrapper.
98 self.data = state
99
100 def json(self):
101 """Serialize the object to json"""
102 return json.dumps(self.data)
103
104 def yaml(self):
105 """Serialize the object to yaml"""
106 return yaml.dump(self.data)
107
108
109def execution_environment():
110 """A convenient bundling of the current execution context"""
111 context = {}
112 context['conf'] = config()
113 if relation_id():
114 context['reltype'] = relation_type()
115 context['relid'] = relation_id()
116 context['rel'] = relation_get()
117 context['unit'] = local_unit()
118 context['rels'] = relations()
119 context['env'] = os.environ
120 return context
121
122
123def in_relation_hook():
124 """Determine whether we're running in a relation hook"""
125 return 'JUJU_RELATION' in os.environ
126
127
128def relation_type():
129 """The scope for the current relation hook"""
130 return os.environ.get('JUJU_RELATION', None)
131
132
133def relation_id():
134 """The relation ID for the current relation hook"""
135 return os.environ.get('JUJU_RELATION_ID', None)
136
137
138def local_unit():
139 """Local unit ID"""
140 return os.environ['JUJU_UNIT_NAME']
141
142
143def remote_unit():
144 """The remote unit for the current relation hook"""
145 return os.environ['JUJU_REMOTE_UNIT']
146
147
148def service_name():
149 """The name service group this unit belongs to"""
150 return local_unit().split('/')[0]
151
152
153def hook_name():
154 """The name of the currently executing hook"""
155 return os.path.basename(sys.argv[0])
156
157
158@cached
159def config(scope=None):
160 """Juju charm configuration"""
161 config_cmd_line = ['config-get']
162 if scope is not None:
163 config_cmd_line.append(scope)
164 config_cmd_line.append('--format=json')
165 try:
166 return json.loads(subprocess.check_output(config_cmd_line))
167 except ValueError:
168 return None
169
170
171@cached
172def relation_get(attribute=None, unit=None, rid=None):
173 """Get relation information"""
174 _args = ['relation-get', '--format=json']
175 if rid:
176 _args.append('-r')
177 _args.append(rid)
178 _args.append(attribute or '-')
179 if unit:
180 _args.append(unit)
181 try:
182 return json.loads(subprocess.check_output(_args))
183 except ValueError:
184 return None
185 except CalledProcessError, e:
186 if e.returncode == 2:
187 return None
188 raise
189
190
191def relation_set(relation_id=None, relation_settings={}, **kwargs):
192 """Set relation information for the current unit"""
193 relation_cmd_line = ['relation-set']
194 if relation_id is not None:
195 relation_cmd_line.extend(('-r', relation_id))
196 for k, v in (relation_settings.items() + kwargs.items()):
197 if v is None:
198 relation_cmd_line.append('{}='.format(k))
199 else:
200 relation_cmd_line.append('{}={}'.format(k, v))
201 subprocess.check_call(relation_cmd_line)
202 # Flush cache of any relation-gets for local unit
203 flush(local_unit())
204
205
206@cached
207def relation_ids(reltype=None):
208 """A list of relation_ids"""
209 reltype = reltype or relation_type()
210 relid_cmd_line = ['relation-ids', '--format=json']
211 if reltype is not None:
212 relid_cmd_line.append(reltype)
213 return json.loads(subprocess.check_output(relid_cmd_line)) or []
214 return []
215
216
217@cached
218def related_units(relid=None):
219 """A list of related units"""
220 relid = relid or relation_id()
221 units_cmd_line = ['relation-list', '--format=json']
222 if relid is not None:
223 units_cmd_line.extend(('-r', relid))
224 return json.loads(subprocess.check_output(units_cmd_line)) or []
225
226
227@cached
228def relation_for_unit(unit=None, rid=None):
229 """Get the json represenation of a unit's relation"""
230 unit = unit or remote_unit()
231 relation = relation_get(unit=unit, rid=rid)
232 for key in relation:
233 if key.endswith('-list'):
234 relation[key] = relation[key].split()
235 relation['__unit__'] = unit
236 return relation
237
238
239@cached
240def relations_for_id(relid=None):
241 """Get relations of a specific relation ID"""
242 relation_data = []
243 relid = relid or relation_ids()
244 for unit in related_units(relid):
245 unit_data = relation_for_unit(unit, relid)
246 unit_data['__relid__'] = relid
247 relation_data.append(unit_data)
248 return relation_data
249
250
251@cached
252def relations_of_type(reltype=None):
253 """Get relations of a specific type"""
254 relation_data = []
255 reltype = reltype or relation_type()
256 for relid in relation_ids(reltype):
257 for relation in relations_for_id(relid):
258 relation['__relid__'] = relid
259 relation_data.append(relation)
260 return relation_data
261
262
263@cached
264def relation_types():
265 """Get a list of relation types supported by this charm"""
266 charmdir = os.environ.get('CHARM_DIR', '')
267 mdf = open(os.path.join(charmdir, 'metadata.yaml'))
268 md = yaml.safe_load(mdf)
269 rel_types = []
270 for key in ('provides', 'requires', 'peers'):
271 section = md.get(key)
272 if section:
273 rel_types.extend(section.keys())
274 mdf.close()
275 return rel_types
276
277
278@cached
279def relations():
280 """Get a nested dictionary of relation data for all related units"""
281 rels = {}
282 for reltype in relation_types():
283 relids = {}
284 for relid in relation_ids(reltype):
285 units = {local_unit(): relation_get(unit=local_unit(), rid=relid)}
286 for unit in related_units(relid):
287 reldata = relation_get(unit=unit, rid=relid)
288 units[unit] = reldata
289 relids[relid] = units
290 rels[reltype] = relids
291 return rels
292
293
294@cached
295def is_relation_made(relation, keys='private-address'):
296 '''
297 Determine whether a relation is established by checking for
298 presence of key(s). If a list of keys is provided, they
299 must all be present for the relation to be identified as made
300 '''
301 if isinstance(keys, str):
302 keys = [keys]
303 for r_id in relation_ids(relation):
304 for unit in related_units(r_id):
305 context = {}
306 for k in keys:
307 context[k] = relation_get(k, rid=r_id,
308 unit=unit)
309 if None not in context.values():
310 return True
311 return False
312
313
314def open_port(port, protocol="TCP"):
315 """Open a service network port"""
316 _args = ['open-port']
317 _args.append('{}/{}'.format(port, protocol))
318 subprocess.check_call(_args)
319
320
321def close_port(port, protocol="TCP"):
322 """Close a service network port"""
323 _args = ['close-port']
324 _args.append('{}/{}'.format(port, protocol))
325 subprocess.check_call(_args)
326
327
328@cached
329def unit_get(attribute):
330 """Get the unit ID for the remote unit"""
331 _args = ['unit-get', '--format=json', attribute]
332 try:
333 return json.loads(subprocess.check_output(_args))
334 except ValueError:
335 return None
336
337
338def unit_private_ip():
339 """Get this unit's private IP address"""
340 return unit_get('private-address')
341
342
343class UnregisteredHookError(Exception):
344 """Raised when an undefined hook is called"""
345 pass
346
347
348class Hooks(object):
349 """A convenient handler for hook functions.
350
351 Example:
352 hooks = Hooks()
353
354 # register a hook, taking its name from the function name
355 @hooks.hook()
356 def install():
357 ...
358
359 # register a hook, providing a custom hook name
360 @hooks.hook("config-changed")
361 def config_changed():
362 ...
363
364 if __name__ == "__main__":
365 # execute a hook based on the name the program is called by
366 hooks.execute(sys.argv)
367 """
368
369 def __init__(self):
370 super(Hooks, self).__init__()
371 self._hooks = {}
372
373 def register(self, name, function):
374 """Register a hook"""
375 self._hooks[name] = function
376
377 def execute(self, args):
378 """Execute a registered hook based on args[0]"""
379 hook_name = os.path.basename(args[0])
380 if hook_name in self._hooks:
381 self._hooks[hook_name]()
382 else:
383 raise UnregisteredHookError(hook_name)
384
385 def hook(self, *hook_names):
386 """Decorator, registering them as hooks"""
387 def wrapper(decorated):
388 for hook_name in hook_names:
389 self.register(hook_name, decorated)
390 else:
391 self.register(decorated.__name__, decorated)
392 if '_' in decorated.__name__:
393 self.register(
394 decorated.__name__.replace('_', '-'), decorated)
395 return decorated
396 return wrapper
397
398
399def charm_dir():
400 """Return the root directory of the current charm"""
401 return os.environ.get('CHARM_DIR')
0402
=== added file 'hooks/charmhelpers/core/host.py'
--- hooks/charmhelpers/core/host.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/host.py 2014-07-21 22:01:08 +0000
@@ -0,0 +1,297 @@
1"""Tools for working with the host system"""
2# Copyright 2012 Canonical Ltd.
3#
4# Authors:
5# Nick Moffitt <nick.moffitt@canonical.com>
6# Matthew Wedgwood <matthew.wedgwood@canonical.com>
7
8import os
9import pwd
10import grp
11import random
12import string
13import subprocess
14import hashlib
15
16from collections import OrderedDict
17
18from hookenv import log
19
20
21def service_start(service_name):
22 """Start a system service"""
23 return service('start', service_name)
24
25
26def service_stop(service_name):
27 """Stop a system service"""
28 return service('stop', service_name)
29
30
31def service_restart(service_name):
32 """Restart a system service"""
33 return service('restart', service_name)
34
35
36def service_reload(service_name, restart_on_failure=False):
37 """Reload a system service, optionally falling back to restart if reload fails"""
38 service_result = service('reload', service_name)
39 if not service_result and restart_on_failure:
40 service_result = service('restart', service_name)
41 return service_result
42
43
44def service(action, service_name):
45 """Control a system service"""
46 cmd = ['service', service_name, action]
47 return subprocess.call(cmd) == 0
48
49
50def service_running(service):
51 """Determine whether a system service is running"""
52 try:
53 output = subprocess.check_output(['service', service, 'status'])
54 except subprocess.CalledProcessError:
55 return False
56 else:
57 if ("start/running" in output or "is running" in output):
58 return True
59 else:
60 return False
61
62
63def adduser(username, password=None, shell='/bin/bash', system_user=False):
64 """Add a user to the system"""
65 try:
66 user_info = pwd.getpwnam(username)
67 log('user {0} already exists!'.format(username))
68 except KeyError:
69 log('creating user {0}'.format(username))
70 cmd = ['useradd']
71 if system_user or password is None:
72 cmd.append('--system')
73 else:
74 cmd.extend([
75 '--create-home',
76 '--shell', shell,
77 '--password', password,
78 ])
79 cmd.append(username)
80 subprocess.check_call(cmd)
81 user_info = pwd.getpwnam(username)
82 return user_info
83
84
85def add_user_to_group(username, group):
86 """Add a user to a group"""
87 cmd = [
88 'gpasswd', '-a',
89 username,
90 group
91 ]
92 log("Adding user {} to group {}".format(username, group))
93 subprocess.check_call(cmd)
94
95
96def rsync(from_path, to_path, flags='-r', options=None):
97 """Replicate the contents of a path"""
98 options = options or ['--delete', '--executability']
99 cmd = ['/usr/bin/rsync', flags]
100 cmd.extend(options)
101 cmd.append(from_path)
102 cmd.append(to_path)
103 log(" ".join(cmd))
104 return subprocess.check_output(cmd).strip()
105
106
107def symlink(source, destination):
108 """Create a symbolic link"""
109 log("Symlinking {} as {}".format(source, destination))
110 cmd = [
111 'ln',
112 '-sf',
113 source,
114 destination,
115 ]
116 subprocess.check_call(cmd)
117
118
119def mkdir(path, owner='root', group='root', perms=0555, force=False):
120 """Create a directory"""
121 log("Making dir {} {}:{} {:o}".format(path, owner, group,
122 perms))
123 uid = pwd.getpwnam(owner).pw_uid
124 gid = grp.getgrnam(group).gr_gid
125 realpath = os.path.abspath(path)
126 if os.path.exists(realpath):
127 if force and not os.path.isdir(realpath):
128 log("Removing non-directory file {} prior to mkdir()".format(path))
129 os.unlink(realpath)
130 else:
131 os.makedirs(realpath, perms)
132 os.chown(realpath, uid, gid)
133
134
135def write_file(path, content, owner='root', group='root', perms=0444):
136 """Create or overwrite a file with the contents of a string"""
137 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
138 uid = pwd.getpwnam(owner).pw_uid
139 gid = grp.getgrnam(group).gr_gid
140 with open(path, 'w') as target:
141 os.fchown(target.fileno(), uid, gid)
142 os.fchmod(target.fileno(), perms)
143 target.write(content)
144
145
146def mount(device, mountpoint, options=None, persist=False):
147 """Mount a filesystem at a particular mountpoint"""
148 cmd_args = ['mount']
149 if options is not None:
150 cmd_args.extend(['-o', options])
151 cmd_args.extend([device, mountpoint])
152 try:
153 subprocess.check_output(cmd_args)
154 except subprocess.CalledProcessError, e:
155 log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
156 return False
157 if persist:
158 # TODO: update fstab
159 pass
160 return True
161
162
163def umount(mountpoint, persist=False):
164 """Unmount a filesystem"""
165 cmd_args = ['umount', mountpoint]
166 try:
167 subprocess.check_output(cmd_args)
168 except subprocess.CalledProcessError, e:
169 log('Error unmounting {}\n{}'.format(mountpoint, e.output))
170 return False
171 if persist:
172 # TODO: update fstab
173 pass
174 return True
175
176
177def mounts():
178 """Get a list of all mounted volumes as [[mountpoint,device],[...]]"""
179 with open('/proc/mounts') as f:
180 # [['/mount/point','/dev/path'],[...]]
181 system_mounts = [m[1::-1] for m in [l.strip().split()
182 for l in f.readlines()]]
183 return system_mounts
184
185
186def file_hash(path):
187 """Generate a md5 hash of the contents of 'path' or None if not found """
188 if os.path.exists(path):
189 h = hashlib.md5()
190 with open(path, 'r') as source:
191 h.update(source.read()) # IGNORE:E1101 - it does have update
192 return h.hexdigest()
193 else:
194 return None
195
196
197def restart_on_change(restart_map, stopstart=False):
198 """Restart services based on configuration files changing
199
200 This function is used a decorator, for example
201
202 @restart_on_change({
203 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
204 })
205 def ceph_client_changed():
206 ...
207
208 In this example, the cinder-api and cinder-volume services
209 would be restarted if /etc/ceph/ceph.conf is changed by the
210 ceph_client_changed function.
211 """
212 def wrap(f):
213 def wrapped_f(*args):
214 checksums = {}
215 for path in restart_map:
216 checksums[path] = file_hash(path)
217 f(*args)
218 restarts = []
219 for path in restart_map:
220 if checksums[path] != file_hash(path):
221 restarts += restart_map[path]
222 services_list = list(OrderedDict.fromkeys(restarts))
223 if not stopstart:
224 for service_name in services_list:
225 service('restart', service_name)
226 else:
227 for action in ['stop', 'start']:
228 for service_name in services_list:
229 service(action, service_name)
230 return wrapped_f
231 return wrap
232
233
234def lsb_release():
235 """Return /etc/lsb-release in a dict"""
236 d = {}
237 with open('/etc/lsb-release', 'r') as lsb:
238 for l in lsb:
239 k, v = l.split('=')
240 d[k.strip()] = v.strip()
241 return d
242
243
244def pwgen(length=None):
245 """Generate a random pasword."""
246 if length is None:
247 length = random.choice(range(35, 45))
248 alphanumeric_chars = [
249 l for l in (string.letters + string.digits)
250 if l not in 'l0QD1vAEIOUaeiou']
251 random_chars = [
252 random.choice(alphanumeric_chars) for _ in range(length)]
253 return(''.join(random_chars))
254
255
256def list_nics(nic_type):
257 '''Return a list of nics of given type(s)'''
258 if isinstance(nic_type, basestring):
259 int_types = [nic_type]
260 else:
261 int_types = nic_type
262 interfaces = []
263 for int_type in int_types:
264 cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
265 ip_output = subprocess.check_output(cmd).split('\n')
266 ip_output = (line for line in ip_output if line)
267 for line in ip_output:
268 if line.split()[1].startswith(int_type):
269 interfaces.append(line.split()[1].replace(":", ""))
270 return interfaces
271
272
273def set_nic_mtu(nic, mtu):
274 '''Set MTU on a network interface'''
275 cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
276 subprocess.check_call(cmd)
277
278
279def get_nic_mtu(nic):
280 cmd = ['ip', 'addr', 'show', nic]
281 ip_output = subprocess.check_output(cmd).split('\n')
282 mtu = ""
283 for line in ip_output:
284 words = line.split()
285 if 'mtu' in words:
286 mtu = words[words.index("mtu") + 1]
287 return mtu
288
289
290def get_nic_hwaddr(nic):
291 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
292 ip_output = subprocess.check_output(cmd)
293 hwaddr = ""
294 words = ip_output.split()
295 if 'link/ether' in words:
296 hwaddr = words[words.index('link/ether') + 1]
297 return hwaddr
0298
=== modified file 'hooks/config-changed'
--- hooks/config-changed 2012-07-21 19:08:52 +0000
+++ hooks/config-changed 2014-07-21 22:01:08 +0000
@@ -1,25 +1,153 @@
1#!/bin/bash1#!/bin/bash
22
3set -e3set -ex
4
5DOMAIN=`config-get domain`
6
7if [ -z "$DOMAIN" ]; then
8 DOMAIN=`unit-get public-address`
9fi
10
11if [ `cat .src` != `config-get src` ]; then
12 if [ `cat .src` == source ]; then
13 apt-get remove charm-helper-sh apache2 libapache2-mod-php5 php5-gd php5-mysql php5-sqlite curl libcurl3 php5-curl rpcbind nfs-common mysql-client -y
14 elif [ `cat .src` == repo ]; then
15 apt-get remove owncloud mysql-client -y
16 apt-get autoremove -y
17 fi
18 hooks/install
19fi
20
21if [ `config-get customssl` == True ] && [ -z `config-get sslkey` ]; then
22 juju-log "If you want to use a custom SSL cert, the key needs to be set. Exiting silently."
23 exit 0
24elif [ `config-get customssl` == True ] && [ -z `config-get sslcert` ]; then
25 juju-log "If you want to use a custom SSL cert, the cert needs to be set. Exiting silently."
26 exit 0
27fi
28
4# Write the apache config29# Write the apache config
5owncloud_vhost="/etc/apache2/sites-available/owncloud"30owncloud_vhost="/etc/apache2/sites-available/owncloud"
6port=`config-get port`31port=`config-get port`
7juju-log "Writing apache config file: ${owncloud_vhost}"32juju-log "Writing apache config file: ${owncloud_vhost}"
8cat > $owncloud_vhost <<EOF33cat > $owncloud_vhost <<EOF
9<VirtualHost *:$port>34<VirtualHost *:$port>
10 DocumentRoot /var/www/owncloud35RewriteEngine on
11 Options All36ReWriteCond %{SERVER_PORT} !^443$
12 ErrorLog /var/log/apache2/owncloud-error.log37RewriteRule ^/(.*) https://%{HTTP_HOST}/$1 [NC,R,L]
13 TransferLog /var/log/apache2/owncloud-access.log38DocumentRoot /var/www/owncloud/
14 RewriteEngine On39<Directory /var/www/owncloud>
15</VirtualHost>40 Options Indexes FollowSymLinks MultiViews
41 AllowOverride All
42</Directory>
43</VirtualHost>
44
45<VirtualHost *:443>
46SSLEngine on
47SSLCertificateFile /home/ubuntu/$DOMAIN.cert
48SSLCertificateKeyFile /home/ubuntu/$DOMAIN.key
49DocumentRoot /var/www/owncloud/
50<Directory /var/www/owncloud>
51 Options Indexes FollowSymLinks MultiViews
52 AllowOverride All
53</Directory>
54RewriteEngine on
55</VirtualHost>
56
16EOF57EOF
1758
59if [ -f .customssl ] && [ `cat .customssl` != `config-get customssl` ]; then
60 rm /home/ubuntu/*.cert
61 rm /home/ubuntu/*.key
62 rm .customssl
63fi
64
65if [ -f .domain ] && [ `cat .domain` != "$DOMAIN" ]; then
66 rm /home/ubuntu/*.cert
67 rm /home/ubuntu/*.key
68 rm .customssl
69fi
70
71if [ ! -f .customssl ] && [ `config-get customssl` == "False" ]; then
72 hooks/ssl
73 echo "False" > .customssl
74elif [ `config-get customssl` == "True" ]; then
75 CERT=`config-get sslcert`
76 KEY=`config-get sslkey`
77 echo "$CERT" > /home/ubuntu/$DOMAIN.cert
78 echo "$KEY" > /home/ubuntu/$DOMAIN.key
79 echo "True" > .customssl
80fi
81
82echo "$DOMAIN" > .domain
83
84
18chmod 0644 $owncloud_vhost85chmod 0644 $owncloud_vhost
1986
87if [ "$port" != "80" ] && [ ! -f .not80 ]; then
88 sed -i "s/NameVirtualHost\ \*\:80/NameVirtualHost\ \*\:$port/" /etc/apache2/ports.conf
89 sed -i "s/Listen\ 80/Listen\ $port/" /etc/apache2/ports.conf
90 echo "$port" > .not80
91elif [ "$port" != "80" ] && [ -f .not80 ]; then
92 not=`cat .not80`
93 sed -i "s/NameVirtualHost\ \*\:$not/NameVirtualHost\ \*\:$port/" /etc/apache2/ports.conf
94 sed -i "s/Listen\ $not/Listen\ $port/" /etc/apache2/ports.conf
95 echo "$port" > .not80
96elif [ "$port" == 80 ] && [ -f .not80 ]; then
97 not=`cat .not80`
98 sed -i "s/NameVirtualHost\ \*\:$not/NameVirtualHost\ \*\:80/" /etc/apache2/ports.conf
99 sed -i "s/Listen\ $not/Listen\ 80/" /etc/apache2/ports.conf
100 rm .not80
101fi
102
103if [ -f .port ] && [ `cat .port` != "$port" ]; then
104 currentport=`cat .port`
105 juju-log "Closing old port, opening new port"
106 close-port "$currentport"/tcp
107 open-port "$port"/tcp
108 echo "$port" > .port
109elif [ ! -f .port ]; then
110 open-port $port/tcp
111 echo "$port" > .port
112fi
113
114if [ -f .dbset ]; then
115 USER=`cat .user`
116 PASSWORD=`cat .password`
117 HOST=`cat .host`
118 DB=`cat .database`
119 if [ `cat .admin` != `config-get user` ]; then
120 OLDADMIN=`cat .admin`
121 ADMIN=`config-get user`
122 mysql -u $USER -p$PASSWORD -h $HOST $DB -e "UPDATE oc_users SET uid = '$ADMIN' WHERE uid = '$OLDADMIN';"
123 mv /var/www/owncloud/data/$OLDADMIN /var/www/owncloud/data/$ADMIN
124 echo "$ADMIN" > .admin
125 fi
126 if [ `cat .adm_pass` != `config-get password` ]; then
127 NEWPASS=`config-get password`
128 OCUSER=`config-get user`
129 if [ ! -f .passwordchanged ]; then
130 SALT=`cat /var/www/owncloud/config/config.php | grep passwordsalt | awk '{print $3}'`
131 sed -i "s/$t_hasher = new PasswordHash(8, FALSE)/$t_hasher = new PasswordHash(8, CRYPT_BLOWFISH!=1)/" /var/www/owncloud/3rdparty/phpass/test.php
132 sed -i "s/'test12345';/'$NEWPASS'.$SALT/" /var/www/owncloud/3rdparty/phpass/test.php
133 sed -i "s/',/';/" /var/www/owncloud/3rdparty/phpass/test.php
134 HASH=`php /var/www/owncloud/3rdparty/phpass/test.php | awk 'NR<2{ print $2 }'`
135 mysql -u $USER -p$PASSWORD -h $HOST $DB -e "UPDATE oc_users SET password = '$HASH' WHERE uid = '$OCUSER';"
136 echo "$NEWPASS" > .adm_pass
137 touch .passwordchanged
138 else
139 OLDPASS=`cat .adm_pass`
140 sed -i "s/'$OLDPASS'./'$NEWPASS'./" /var/www/owncloud/3rdparty/phpass/test.php
141 HASH=`php /var/www/owncloud/3rdparty/phpass/test.php | awk 'NR<2{ print $2 }'`
142 mysql -u $USER -p$PASSWORD -h $HOST $DB -e "UPDATE oc_users SET password = '$HASH' WHERE uid = '$OCUSER';"
143 echo "$NEWPASS" > .adm_pass
144 fi
145 fi
146fi
147
148
20# Optimize PHP for large files149# Optimize PHP for large files
21juju-log "Increasing upload limit and maximum POST size"150juju-log "Increasing upload limit and maximum POST size"
22sed -i 's/;upload_max_filesize/;upload_max_filesize/g' /etc/php5/apache2/php.ini
23sed -i 's/^post_max_size/;post_max_size/g' /etc/php5/apache2/php.ini151sed -i 's/^post_max_size/;post_max_size/g' /etc/php5/apache2/php.ini
24152
25cat > /etc/php5/conf.d/owncloud.ini <<EOF153cat > /etc/php5/conf.d/owncloud.ini <<EOF
@@ -31,6 +159,7 @@
31juju-log "Enabling Apache modules: rewrite, headers, vhost_alias"159juju-log "Enabling Apache modules: rewrite, headers, vhost_alias"
32a2enmod rewrite160a2enmod rewrite
33a2enmod headers161a2enmod headers
162a2enmod ssl
34163
35# Enable Apache vhost164# Enable Apache vhost
36juju-log "Enabling ownCloud Apache vhost"165juju-log "Enabling ownCloud Apache vhost"
@@ -40,3 +169,18 @@
40juju-log "Reloading Apache configuration"169juju-log "Reloading Apache configuration"
41service apache2 start || :170service apache2 start || :
42service apache2 reload171service apache2 reload
172
173
174
175if [ ! -f .443 ]; then
176 open-port 443
177 touch .443
178fi
179
180if [ ! -f .configured ]; then
181 touch .configured
182fi
183
184if [ ! -f .started ]; then
185 hooks/start
186fi
43187
=== modified file 'hooks/db-relation-changed'
--- hooks/db-relation-changed 2012-07-21 16:20:15 +0000
+++ hooks/db-relation-changed 2014-07-21 22:01:08 +0000
@@ -12,6 +12,11 @@
12password=`relation-get password`12password=`relation-get password`
13host=`relation-get private-address`13host=`relation-get private-address`
1414
15echo "$database" > .database
16echo "$password" > .password
17echo "$user" > .user
18echo "$host" > .host
19
15# Prepare for sqlite->mysql migration if configured20# Prepare for sqlite->mysql migration if configured
16if [ -f "/var/www/owncloud/config/config.php" ]; then21if [ -f "/var/www/owncloud/config/config.php" ]; then
17 if grep -qs "sqlite" /var/www/owncloud/config/config.php; then22 if grep -qs "sqlite" /var/www/owncloud/config/config.php; then
@@ -35,6 +40,9 @@
35 adm_pass=`cat /dev/urandom | tr -dc [:alnum:] | head -c 10`40 adm_pass=`cat /dev/urandom | tr -dc [:alnum:] | head -c 10`
36fi41fi
3742
43echo "$admin" > .admin
44echo "$adm_pass" > .adm_pass
45
38if [ -f "/var/www/owncloud/config/config.php" ]; then46if [ -f "/var/www/owncloud/config/config.php" ]; then
39 # Update config settings47 # Update config settings
40 cat > /var/www/owncloud/config/config.php <<EOF48 cat > /var/www/owncloud/config/config.php <<EOF
@@ -80,3 +88,4 @@
80fi88fi
8189
82chmod 0644 /var/www/owncloud/config/*90chmod 0644 /var/www/owncloud/config/*
91touch .dbset
8392
=== added file 'hooks/db-relation-departed'
--- hooks/db-relation-departed 1970-01-01 00:00:00 +0000
+++ hooks/db-relation-departed 2014-07-21 22:01:08 +0000
@@ -0,0 +1,13 @@
1#!/bin/bash
2
3set -eux
4
5mv /var/www/owncloud/config/config.php{,.bakmysql}
6
7if [ -f "/var/www/owncloud/config/config.bak" ]; then
8 if grep -qs "mysql" /var/www/owncloud/config/config.bak; then
9 mv /var/www/owncloud/config/config.bak{,.php}
10 fi
11fi
12
13chmod 0644 /var/www/owncloud/config/*
014
=== modified file 'hooks/install'
--- hooks/install 2014-04-11 03:48:08 +0000
+++ hooks/install 2014-07-21 22:01:08 +0000
@@ -1,30 +1,52 @@
1#!/bin/bash1#!/bin/bash
22
3set -eu # -x for verbose logging to ensemble debug-log3set -eu
44
5juju-log "Installing all components"5juju-log "Installing all components"
66
7add-apt-repository -y ppa:charmers/charm-helpers7if [ `config-get src` == "source" ]; then
8apt-get update8
9apt-get install -qqy charm-helper-sh apache2 libapache2-mod-php5 \9 add-apt-repository -y ppa:charmers/charm-helpers
10 php5-gd php5-mysql php5-sqlite curl libcurl3 \10 apt-get update
11 php5-curl rpcbind nfs-common11 apt-get install -qqy charm-helper-sh apache2 libapache2-mod-php5 php5-gd php5-mysql php5-sqlite curl libcurl3 php5-curl rpcbind nfs-common mysql-client
1212
13juju-log "Downloading and validating ownCloud"13 juju-log "Downloading and validating ownCloud"
14# Load charm helper and download owncloud14 # Load charm helper and download owncloud
15. /usr/share/charm-helper/sh/net.sh15 . /usr/share/charm-helper/sh/net.sh
16LURL="http://download.owncloud.org/community/owncloud-6.0.2.tar.bz2"16 LURL=`config-get downloadurl`
1717
18LHASH="a5a194ad07fca7cbf158b660cc098c6364590bdd15d086069221faf4386b713f"18 LHASH=`config-get sha1sum`
19source_file=`ch_get_file $LURL $LHASH`19 source_file=`ch_get_file $LURL $LHASH`
2020
21juju-log "Downloaded"21 juju-log "Downloaded"
2222
23# Prepare files23 # Prepare files
24juju-log "Creating ownCloud DocRoot"24 juju-log "Creating ownCloud DocRoot"
25tar -xjf $source_file --directory /var/www25 tar -xjf $source_file --directory /var/www
26
27elif [ `config-get src` == "repo" ]; then
28
29 apt-get install -y mysql-client
30
31 sh -c "echo 'deb http://download.opensuse.org/repositories/isv:/ownCloud:/community/xUbuntu_12.04/ /' >> /etc/apt/sources.list.d/owncloud.list"
32 wget http://download.opensuse.org/repositories/isv:/ownCloud:/community/xUbuntu_12.04/Release.key
33
34 if [ `sha1sum Release.key | awk '{print $1}'` != "c76c49ca044d9547648f1d8313777ed7d55df6b2" ]; then
35 juju-log "Apt key verification failed, failing hook."
36 exit 1
37 fi
38
39 apt-key add - < Release.key
40 apt-get update
41 apt-get install -y owncloud
42
43fi
44
26chown www-data:www-data -R /var/www/owncloud45chown www-data:www-data -R /var/www/owncloud
2746
28#Make ownCloud publicly visible47sed -i "s/ \$curlSettings = array(/ \$curlSettings = array(\n CURLOPT_SSL_VERIFYPEER => 0,\n CURLOPT_SSL_VERIFYHOST => 0,/" /var/www/owncloud/3rdparty/Sabre/DAV/Client.php
29port=`config-get port`48
30open-port $port/tcp49SRC=`config-get src`
50echo $SRC > .src
51
52service apache2 restart
3153
=== added file 'hooks/ssl'
--- hooks/ssl 1970-01-01 00:00:00 +0000
+++ hooks/ssl 2014-07-21 22:01:08 +0000
@@ -0,0 +1,19 @@
1#!/usr/bin/python
2
3import os
4import sys
5
6sys.path.insert(0, os.environ['CHARM_DIR'])
7
8from charmhelpers.contrib import ssl
9from charmhelpers.core import hookenv
10from charmhelpers.core.hookenv import unit_get
11
12DOMAIN = hookenv.config('domain')
13
14if DOMAIN == "":
15 DOMAIN = unit_get('public-address')
16
17key_file = '/home/ubuntu/' + DOMAIN + '.key'
18cert_file = '/home/ubuntu/' + DOMAIN + '.cert'
19ssl.generate_selfsigned(key_file, cert_file, cn=DOMAIN)
020
=== modified file 'hooks/start'
--- hooks/start 2012-07-21 00:54:38 +0000
+++ hooks/start 2014-07-21 22:01:08 +0000
@@ -1,4 +1,8 @@
1#!/bin/bash1#!/bin/bash
2set -eu2set -eu
3juju-log "Starting apache for owncloud"3
4service apache2 start || service apache2 restart4if [ -f .configured ]; then
5 juju-log "Starting apache for owncloud"
6 service apache2 start || service apache2 restart
7 touch .started
8fi
59
=== modified file 'hooks/upgrade-charm'
--- hooks/upgrade-charm 2014-04-11 03:47:03 +0000
+++ hooks/upgrade-charm 2014-07-21 22:01:08 +0000
@@ -5,43 +5,47 @@
5# Pause during upgrade5# Pause during upgrade
6hooks/stop6hooks/stop
77
8#we are under active migration, dont do anything more than once8if [ -f /usr/share/charm-helper/sh/net.sh ]; then
9if [ ! -f $CHARM_DIR/.migrating ]; then9 #we are under active migration, dont do anything more than once
1010 if [ ! -f $CHARM_DIR/.migrating ]; then
11 # Excise old installation data11
12 find /var/www -mindepth 2 -maxdepth 2 -type d -name 'data'\12 # Excise old installation data
13 -exec mv {} /var/tmp/ \;13 find /var/www -mindepth 2 -maxdepth 2 -type d -name 'data'\
14 find /var/www -mindepth 2 -maxdepth 2 -type d -name 'config'\14 -exec mv {} /var/tmp/ \;
15 -exec mv {} /var/tmp/ \;15 find /var/www -mindepth 2 -maxdepth 2 -type d -name 'config'\
16 touch $CHARM_DIR/.migrating16 -exec mv {} /var/tmp/ \;
1717 touch $CHARM_DIR/.migrating
18
19 fi
20
21 # Deleting all traces of the old ownCloud install
22 rm -rf /var/www/owncloud/
23
24 cd /var/www/
25
26 # Getting new ownCloud version and MD5 checking it
27 wget download.owncloud.org/community/owncloud-6.0.3.tar.bz2
28
29 if [ `sha1sum owncloud-6.0.3.tar.bz2 | awk '{print $1}'` != "df1e272c208376117a8c00619079cee25a22f784" ]; then
30 juju-log "Download verification failed. Exiting."
31 exit 1
32 fi
33
34 tar xfj owncloud-6.0.3.tar.bz2
35 cd $CHARM_DIR
36
37 # Migrating data
38 mkdir /var/www/owncloud/data
39 rm -rf /var/www/owncloud/config
40 mkdir /var/www/owncloud/config
41 cp -pr /var/tmp/data/* /var/www/owncloud/data/
42 cp -pr /var/tmp/config/* /var/www/owncloud/config/
43 sudo chown -R www-data:www-data /var/www/owncloud
44 rm $CHARM_DIR/.migrating
45
46 # Finish upgrade
47 hooks/config-changed
48else
49 apt-get update
50 apt-get upgrade -y
18fi51fi
19
20# Deleting all traces of the old ownCloud install
21rm -rf /var/www/owncloud/
22cd /var/www/
23
24# Getting new ownCloud version and MD5 checking it
25wget download.owncloud.org/community/owncloud-6.0.2.tar.bz2
26wget download.owncloud.org/community/owncloud-6.0.2.tar.bz2.md5
27
28while [ `md5sum owncloud-6.0.2.tar.bz2 | awk '{print $1}'` != `cat owncloud-6.0.2.tar.bz2.md5 | awk '{print $1}'` ]; do
29 rm owncloud-6.0.2.tar.bz2
30 wget download.owncloud.org/community/owncloud-6.0.2.tar.bz2
31done
32
33tar xfj owncloud-6.0.2.tar.bz2
34cd $CHARM_DIR
35
36# Migrating data
37mkdir /var/www/owncloud/data
38rm -rf /var/www/owncloud/config
39mkdir /var/www/owncloud/config
40cp -pr /var/tmp/data/* /var/www/owncloud/data/
41cp -pr /var/tmp/config/* /var/www/owncloud/config/
42sudo chown -R www-data:www-data /var/www/owncloud
43
44# Finish upgrade
45hooks/config-changed
46
47rm $CHARM_DIR/.migrating
4852
=== modified file 'hooks/website-relation-joined'
--- hooks/website-relation-joined 2012-07-21 16:20:15 +0000
+++ hooks/website-relation-joined 2014-07-21 22:01:08 +0000
@@ -1,4 +1,3 @@
1#!/bin/sh1#!/bin/sh
2port=`config-get port`
3pub_addr=`unit-get private-address`2pub_addr=`unit-get private-address`
4relation-set port=$port hostname=$pub_addr3relation-set port=443 hostname=$pub_addr
54
=== modified file 'metadata.yaml'
--- metadata.yaml 2014-01-24 16:01:17 +0000
+++ metadata.yaml 2014-07-21 22:01:08 +0000
@@ -6,7 +6,7 @@
6 storage, MySQL Databases, and HAProxy reverse proxy charms. 6 storage, MySQL Databases, and HAProxy reverse proxy charms.
7categories:7categories:
8 - file-servers8 - file-servers
9maintainer: "Nathan Williams <nathan@nathanewilliams.com>"9maintainer: "Jose Antonio Rey <jose@ubuntu.com>"
10requires:10requires:
11 db:11 db:
12 interface: mysql12 interface: mysql
1313
=== modified file 'tests/00_setup.sh' (properties changed: -x to +x)
=== added file 'tests/100-deploy.py'
--- tests/100-deploy.py 1970-01-01 00:00:00 +0000
+++ tests/100-deploy.py 2014-07-21 22:01:08 +0000
@@ -0,0 +1,116 @@
1#!/usr/bin/env python3
2# The format of this test was shamelessly ripped from Cory Johns awesome
3# tests that resemble nosetests in the Apache Allura charm. <3
4# http://bazaar.launchpad.net/~johnsca/charms/precise/apache-allura/
5
6import amulet
7import requests
8
9
10class TestDeploy(object):
11
12 def __init__(self, time=2500):
13 # Attempt to load the deployment topology from a bundle.
14 self.deploy = amulet.Deployment()
15
16 # If something errored out, attempt to continue by
17 # manually specifying a standalone deployment
18 self.deploy.add('owncloud')
19 self.deploy.add('mysql')
20 self.deploy.add('nfs')
21 self.deploy.configure('owncloud', {'user': 'tom',
22 'password': 'swordfish'})
23 self.deploy.relate('owncloud:db', 'mysql:db')
24 self.deploy.relate('owncloud:shared-fs', 'nfs:nfs')
25 self.deploy.expose('owncloud')
26
27 try:
28 self.deploy.setup(time)
29 self.deploy.sentry.wait(time)
30 except:
31 amulet.raise_status(amulet.FAIL, msg="Environment standup timeout")
32 sentry = self.deploy.sentry
33
34 self.domain = sentry.unit['owncloud/0'].info['public-address']
35 self.mysql_unit = self.deploy.sentry.unit['mysql/0']
36 self.nfs_unit = self.deploy.sentry.unit['nfs/0']
37 self.app_unit = self.deploy.sentry.unit['owncloud/0']
38
39 def run(self):
40 for test in dir(self):
41 if test.startswith('test_'):
42 getattr(self, test)()
43
44 def test_standalone_http(self):
45
46 # r = requests.get("http://%s/" % domain, data, headers=h)
47 r = requests.get("https://%s/index.php" % self.domain, verify=False)
48 r.raise_for_status()
49
50 search_string = 'web services under your control'
51 if r.text.find(search_string) is -1:
52 amulet.raise_status(amulet.FAIL, msg="Unable to verify login page")
53
54 def test_database_relationship(self):
55
56 sent_config = self.mysql_unit.relation('db', 'owncloud:db')
57 # I'm sorry this is gross, and repeats itself. This is a dirty hack
58 # around owncloud not normalizing quotations, and field names.
59 try:
60 filepath = "/var/www/owncloud/config/autoconfig.php"
61 cfg = self.deploy.sentry.unit['owncloud/0'].file_contents(filepath)
62 cfg_to_check = {'dbuser': 'user', 'dbpass': 'password',
63 'dbhost': 'host'}
64 for c_key, j_key in cfg_to_check.items():
65 if cfg.find('"%s" => "%s"' % (c_key, sent_config[j_key])) == -1:
66 amulet.raise_status(amulet.FAIL,
67 msg="Unable to validate db cfg %s" % j_key)
68 except:
69 filepath = "/var/www/owncloud/config/config.php"
70 cfg = self.deploy.sentry.unit['owncloud/0'].file_contents(filepath)
71 cfg_to_check = {'dbuser': 'user', 'dbpassword': 'password',
72 'dbhost': 'host'}
73 for c_key, j_key in cfg_to_check.items():
74 if cfg.find("'%s' => '%s'" % (c_key, sent_config[j_key])) == -1:
75 amulet.raise_status(amulet.FAIL,
76 msg="Unable to validate db cfg %s" % j_key)
77
78 def term_search(self, output, term):
79 if output.find(term) == -1:
80 amulet.raise_status(amulet.FAIL,
81 msg="Unable to validate NFS config %s" % term)
82
83 def test_nfs_relationship(self):
84 # Cache Relationship details
85 nfs_relation = self.nfs_unit.relation('nfs', 'owncloud:shared-fs')
86
87 # Leverage Amulet's exception handling if directory doesnt exist
88 # to check for the directory, as a cheap quick fail test.
89 try:
90 self.app_unit.directory('/var/lib/owncloud')
91 except:
92 amulet.raise_status(amulet.FAIL, msg="NFS Directory not found")
93
94 #Fetch the contents of mtab for data validation
95 mtab_contents = self.app_unit.file_contents('/etc/mtab')
96
97 self.term_search(mtab_contents, nfs_relation['private-address'])
98 self.term_search(mtab_contents, nfs_relation['fstype'])
99 self.term_search(mtab_contents, nfs_relation['mountpoint'])
100 self.term_search(mtab_contents, nfs_relation['options'])
101
102 def test_nfs_write_pipeline(self):
103 # Validate file write pipeline
104 #Build a $block_size file, and ship it via NFS
105 cmd_builder = "dd if=/dev/zero of=/var/lib/owncloud/amulet-file-test bs=8M count=1"
106 self.app_unit.run(cmd_builder)
107
108 filepath = '/srv/data/relation-sentry/amulet-file-test'
109 file_test = self.nfs_unit.file(filepath)
110 if file_test['size'] < 8000000:
111 amulet.raise_status(amulet.FAIL, 'File size constraint not met')
112
113
114if __name__ == '__main__':
115 runner = TestDeploy()
116 runner.run()
0117
=== removed file 'tests/100_deploy.test'
--- tests/100_deploy.test 2014-02-10 21:49:21 +0000
+++ tests/100_deploy.test 1970-01-01 00:00:00 +0000
@@ -1,142 +0,0 @@
1#!/usr/bin/env python3
2
3import amulet
4import requests
5
6##########################################
7# Config Options
8##########################################
9scale = 2
10seconds = 1200
11user = 'tom'
12password = 'swordfish'
13block_size = "8M"
14verify_size = 8000000
15
16###########################################################
17#Deployment Setup
18############################################################
19d = amulet.Deployment()
20
21d.add('owncloud', units=scale)
22d.add('mysql')
23d.add('haproxy')
24d.add('nfs')
25d.configure('owncloud', {'user': user, 'password': password})
26d.relate('owncloud:db', 'mysql:db')
27d.relate('owncloud:website', 'haproxy:reverseproxy')
28d.relate('owncloud:shared-fs', 'nfs:nfs')
29d.expose('owncloud')
30d.expose('haproxy')
31
32
33#perform deployment
34try:
35 d.setup(timeout=seconds)
36except amulet.helpers.TimeoutError:
37 message = 'The environment did not setup in %d seconds.', seconds
38 amulet.raise_status(amulet.SKIP, msg=message)
39except:
40 raise
41
42
43#############################################################
44# Check presence of HTTP services
45#############################################################
46def validate_status_interface():
47 h = {'User-Agent': 'Mozilla/5.0 Gecko/20100101 Firefox/12.0',
48 'Content-Type': 'application/x-www-form-urlencoded',
49 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*',
50 'Accept-Encoding': 'gzip, deflate'}
51
52 data = {'user': 'tom', 'password': 'swordfish'}
53
54 #validate the Owncloud login screen is present
55 r = requests.post("http://{}".format(
56 d.sentry.unit['owncloud/0'].info['public-address']),
57 data, headers=h)
58 r.raise_for_status()
59
60 #validate the HAProxy daemon is forwarding as expected
61 r = requests.get("http://{}".format(
62 d.sentry.unit['haproxy/0'].info['public-address']))
63 r.raise_for_status
64
65
66#############################################################
67# Validate that each service is running
68#############################################################
69def validate_running_services():
70 output, code = d.sentry.unit['owncloud/0'].run('service apache2 status')
71 if code != 0:
72 message = "Failed to find running Apache instance on host owncloud/0"
73 amulet.raise_status(amulet.SKIP, msg=message)
74 output, code = d.sentry.unit['mysql/0'].run('service mysql status')
75 if code != 0:
76 message = "Failed to find running MYSQL instance on host mysql/0"
77 amulet.raise_status(amulet.SKIP, msg=message)
78
79
80#############################################################
81# Validate that each service is running
82#############################################################
83def validate_owncloud_files():
84 index_status = d.sentry.unit['owncloud/0'].file_stat('/var/www/owncloud/index.php')
85 if index_status['size'] <= 0:
86 message = "Failed to find owncloud index.php"
87 amulet.raise_status(amulet.SKIP, msg=message)
88
89
90#############################################################
91# Validate database relationship
92#############################################################
93def validate_database_relationship():
94 #Connect to the sentrys and fetch the transmitted details
95 sent_config = d.sentry.unit['mysql/0'].relation('db', 'owncloud:db')
96 #Connect to owncloud's sentry and read the configuration PHP file
97 prod_config = d.sentry.unit['owncloud/0'].file_contents('/var/www/owncloud/config/config.php')
98 cfg_to_check = {'dbuser': 'user', 'dbpassword': 'password', 'dbhost': 'host'}
99
100 #Search the return string of the config for the transmit values
101 for cfg_file_key, juju_cfg_key in cfg_to_check.items():
102 if prod_config.find("'%s' => '%s'" % (cfg_file_key, sent_config[juju_cfg_key])) == -1:
103 amulet.raise_status(amulet.SKIP, msg="Unable to validate db sent %s" % juju_cfg_key)
104
105
106# Utility Method for searching output
107def nfs_term_search(output, term):
108 if output.find(term) == -1:
109 amulet.raise_status(amulet.FAIL, msg="Unable to validate NFS export mounted with %s" % term)
110
111
112###########################################################
113# Validate NFS FileSystem Existence
114###########################################################
115def validate_nfs_relationship():
116 # Cache Relationship details
117 nfs_relation = d.sentry.unit['nfs/0'].relation('nfs', 'owncloud:shared-fs')
118 # Raises an error if the directory does not exist
119 d.sentry.unit['owncloud/0'].directory('/var/lib/owncloud')
120 #Fetch the contents of mtab for data validation
121 mtab_contents = d.sentry.unit['owncloud/0'].file_contents('/etc/mtab')
122
123 nfs_term_search(mtab_contents, nfs_relation['private-address'])
124 nfs_term_search(mtab_contents, nfs_relation['fstype'])
125 nfs_term_search(mtab_contents, nfs_relation['mountpoint'])
126 nfs_term_search(mtab_contents, nfs_relation['options'])
127
128 # Validate file write pipeline
129 #Build a $block_size file, and ship it via NFS
130 cmd_builder = "dd if=/dev/zero of=/var/lib/owncloud/amulet-file-test bs=%s count=1" % block_size
131 d.sentry.unit['owncloud/0'].run(cmd_builder)
132
133 file_test = d.sentry.unit['nfs/0'].file('/srv/data/relation-sentry/amulet-file-test')
134 if file_test['size'] < verify_size:
135 amulet.raise_status(amulet.FAIL, 'File size constraint not met')
136
137
138validate_status_interface()
139validate_running_services()
140validate_owncloud_files()
141validate_database_relationship()
142validate_nfs_relationship()

Subscribers

People subscribed via source and target branches

to all changes:
to status/vote changes: