Merge lp:~hopem/charms/trusty/keystone/lp1499643 into lp:~openstack-charmers-archive/charms/trusty/keystone/next

Proposed by Edward Hope-Morley
Status: Merged
Merged at revision: 179
Proposed branch: lp:~hopem/charms/trusty/keystone/lp1499643
Merge into: lp:~openstack-charmers-archive/charms/trusty/keystone/next
Diff against target: 2301 lines (+1618/-93)
16 files modified
charmhelpers/contrib/network/ip.py (+5/-3)
charmhelpers/contrib/openstack/amulet/deployment.py (+23/-9)
charmhelpers/contrib/openstack/amulet/utils.py (+359/-0)
charmhelpers/contrib/openstack/context.py (+60/-16)
charmhelpers/contrib/openstack/templating.py (+30/-2)
charmhelpers/contrib/openstack/utils.py (+232/-2)
charmhelpers/contrib/storage/linux/ceph.py (+226/-13)
charmhelpers/core/hookenv.py (+32/-0)
charmhelpers/core/host.py (+32/-16)
charmhelpers/core/hugepage.py (+8/-1)
charmhelpers/core/strutils.py (+30/-0)
tests/charmhelpers/contrib/amulet/deployment.py (+4/-2)
tests/charmhelpers/contrib/amulet/utils.py (+193/-19)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+23/-9)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+359/-0)
unit_tests/test_keystone_hooks.py (+2/-1)
To merge this branch: bzr merge lp:~hopem/charms/trusty/keystone/lp1499643
Reviewer Review Type Date Requested Status
Liam Young (community) Approve
Review via email: mp+272411@code.launchpad.net
To post a comment you must log in.
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #10767 keystone-next for hopem mp272411
    LINT OK: passed

Build: http://10.245.162.77:8080/job/charm_lint_check/10767/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_unit_test #9944 keystone-next for hopem mp272411
    UNIT OK: passed

Build: http://10.245.162.77:8080/job/charm_unit_test/9944/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #6772 keystone-next for hopem mp272411
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
make: *** [functional_test] Error 1
ERROR:root:Make target returned non-zero.

Full amulet test output: http://paste.ubuntu.com/12555940/
Build: http://10.245.162.77:8080/job/charm_amulet_test/6772/

Revision history for this message
Liam Young (gnuoy) wrote :

Amulet fail is unrelated. Approved

review: Approve
Revision history for this message
Ryan Beisner (1chb1n) wrote :

(!) All keystone/next amulet tests with, and after, this commit, are failing in the same place as the test here.

Last known good was keystone/test rev178

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'charmhelpers/contrib/network/ip.py'
--- charmhelpers/contrib/network/ip.py 2015-09-03 09:40:28 +0000
+++ charmhelpers/contrib/network/ip.py 2015-09-25 14:42:33 +0000
@@ -23,7 +23,7 @@
23from functools import partial23from functools import partial
2424
25from charmhelpers.core.hookenv import unit_get25from charmhelpers.core.hookenv import unit_get
26from charmhelpers.fetch import apt_install26from charmhelpers.fetch import apt_install, apt_update
27from charmhelpers.core.hookenv import (27from charmhelpers.core.hookenv import (
28 log,28 log,
29 WARNING,29 WARNING,
@@ -32,13 +32,15 @@
32try:32try:
33 import netifaces33 import netifaces
34except ImportError:34except ImportError:
35 apt_install('python-netifaces')35 apt_update(fatal=True)
36 apt_install('python-netifaces', fatal=True)
36 import netifaces37 import netifaces
3738
38try:39try:
39 import netaddr40 import netaddr
40except ImportError:41except ImportError:
41 apt_install('python-netaddr')42 apt_update(fatal=True)
43 apt_install('python-netaddr', fatal=True)
42 import netaddr44 import netaddr
4345
4446
4547
=== modified file 'charmhelpers/contrib/openstack/amulet/deployment.py'
--- charmhelpers/contrib/openstack/amulet/deployment.py 2015-08-19 13:53:50 +0000
+++ charmhelpers/contrib/openstack/amulet/deployment.py 2015-09-25 14:42:33 +0000
@@ -44,20 +44,31 @@
44 Determine if the local branch being tested is derived from its44 Determine if the local branch being tested is derived from its
45 stable or next (dev) branch, and based on this, use the corresonding45 stable or next (dev) branch, and based on this, use the corresonding
46 stable or next branches for the other_services."""46 stable or next branches for the other_services."""
47
48 # Charms outside the lp:~openstack-charmers namespace
47 base_charms = ['mysql', 'mongodb', 'nrpe']49 base_charms = ['mysql', 'mongodb', 'nrpe']
4850
51 # Force these charms to current series even when using an older series.
52 # ie. Use trusty/nrpe even when series is precise, as the P charm
53 # does not possess the necessary external master config and hooks.
54 force_series_current = ['nrpe']
55
49 if self.series in ['precise', 'trusty']:56 if self.series in ['precise', 'trusty']:
50 base_series = self.series57 base_series = self.series
51 else:58 else:
52 base_series = self.current_next59 base_series = self.current_next
5360
54 if self.stable:61 for svc in other_services:
55 for svc in other_services:62 if svc['name'] in force_series_current:
63 base_series = self.current_next
64 # If a location has been explicitly set, use it
65 if svc.get('location'):
66 continue
67 if self.stable:
56 temp = 'lp:charms/{}/{}'68 temp = 'lp:charms/{}/{}'
57 svc['location'] = temp.format(base_series,69 svc['location'] = temp.format(base_series,
58 svc['name'])70 svc['name'])
59 else:71 else:
60 for svc in other_services:
61 if svc['name'] in base_charms:72 if svc['name'] in base_charms:
62 temp = 'lp:charms/{}/{}'73 temp = 'lp:charms/{}/{}'
63 svc['location'] = temp.format(base_series,74 svc['location'] = temp.format(base_series,
@@ -66,6 +77,7 @@
66 temp = 'lp:~openstack-charmers/charms/{}/{}/next'77 temp = 'lp:~openstack-charmers/charms/{}/{}/next'
67 svc['location'] = temp.format(self.current_next,78 svc['location'] = temp.format(self.current_next,
68 svc['name'])79 svc['name'])
80
69 return other_services81 return other_services
7082
71 def _add_services(self, this_service, other_services):83 def _add_services(self, this_service, other_services):
@@ -77,21 +89,23 @@
7789
78 services = other_services90 services = other_services
79 services.append(this_service)91 services.append(this_service)
92
93 # Charms which should use the source config option
80 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',94 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
81 'ceph-osd', 'ceph-radosgw']95 'ceph-osd', 'ceph-radosgw']
82 # Most OpenStack subordinate charms do not expose an origin option96
83 # as that is controlled by the principle.97 # Charms which can not use openstack-origin, ie. many subordinates
84 ignore = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']98 no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']
8599
86 if self.openstack:100 if self.openstack:
87 for svc in services:101 for svc in services:
88 if svc['name'] not in use_source + ignore:102 if svc['name'] not in use_source + no_origin:
89 config = {'openstack-origin': self.openstack}103 config = {'openstack-origin': self.openstack}
90 self.d.configure(svc['name'], config)104 self.d.configure(svc['name'], config)
91105
92 if self.source:106 if self.source:
93 for svc in services:107 for svc in services:
94 if svc['name'] in use_source and svc['name'] not in ignore:108 if svc['name'] in use_source and svc['name'] not in no_origin:
95 config = {'source': self.source}109 config = {'source': self.source}
96 self.d.configure(svc['name'], config)110 self.d.configure(svc['name'], config)
97111
98112
=== modified file 'charmhelpers/contrib/openstack/amulet/utils.py'
--- charmhelpers/contrib/openstack/amulet/utils.py 2015-07-16 20:17:38 +0000
+++ charmhelpers/contrib/openstack/amulet/utils.py 2015-09-25 14:42:33 +0000
@@ -27,6 +27,7 @@
27import heatclient.v1.client as heat_client27import heatclient.v1.client as heat_client
28import keystoneclient.v2_0 as keystone_client28import keystoneclient.v2_0 as keystone_client
29import novaclient.v1_1.client as nova_client29import novaclient.v1_1.client as nova_client
30import pika
30import swiftclient31import swiftclient
3132
32from charmhelpers.contrib.amulet.utils import (33from charmhelpers.contrib.amulet.utils import (
@@ -602,3 +603,361 @@
602 self.log.debug('Ceph {} samples (OK): '603 self.log.debug('Ceph {} samples (OK): '
603 '{}'.format(sample_type, samples))604 '{}'.format(sample_type, samples))
604 return None605 return None
606
607# rabbitmq/amqp specific helpers:
608 def add_rmq_test_user(self, sentry_units,
609 username="testuser1", password="changeme"):
610 """Add a test user via the first rmq juju unit, check connection as
611 the new user against all sentry units.
612
613 :param sentry_units: list of sentry unit pointers
614 :param username: amqp user name, default to testuser1
615 :param password: amqp user password
616 :returns: None if successful. Raise on error.
617 """
618 self.log.debug('Adding rmq user ({})...'.format(username))
619
620 # Check that user does not already exist
621 cmd_user_list = 'rabbitmqctl list_users'
622 output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
623 if username in output:
624 self.log.warning('User ({}) already exists, returning '
625 'gracefully.'.format(username))
626 return
627
628 perms = '".*" ".*" ".*"'
629 cmds = ['rabbitmqctl add_user {} {}'.format(username, password),
630 'rabbitmqctl set_permissions {} {}'.format(username, perms)]
631
632 # Add user via first unit
633 for cmd in cmds:
634 output, _ = self.run_cmd_unit(sentry_units[0], cmd)
635
636 # Check connection against the other sentry_units
637 self.log.debug('Checking user connect against units...')
638 for sentry_unit in sentry_units:
639 connection = self.connect_amqp_by_unit(sentry_unit, ssl=False,
640 username=username,
641 password=password)
642 connection.close()
643
644 def delete_rmq_test_user(self, sentry_units, username="testuser1"):
645 """Delete a rabbitmq user via the first rmq juju unit.
646
647 :param sentry_units: list of sentry unit pointers
648 :param username: amqp user name, default to testuser1
649 :param password: amqp user password
650 :returns: None if successful or no such user.
651 """
652 self.log.debug('Deleting rmq user ({})...'.format(username))
653
654 # Check that the user exists
655 cmd_user_list = 'rabbitmqctl list_users'
656 output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
657
658 if username not in output:
659 self.log.warning('User ({}) does not exist, returning '
660 'gracefully.'.format(username))
661 return
662
663 # Delete the user
664 cmd_user_del = 'rabbitmqctl delete_user {}'.format(username)
665 output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_del)
666
667 def get_rmq_cluster_status(self, sentry_unit):
668 """Execute rabbitmq cluster status command on a unit and return
669 the full output.
670
671 :param unit: sentry unit
672 :returns: String containing console output of cluster status command
673 """
674 cmd = 'rabbitmqctl cluster_status'
675 output, _ = self.run_cmd_unit(sentry_unit, cmd)
676 self.log.debug('{} cluster_status:\n{}'.format(
677 sentry_unit.info['unit_name'], output))
678 return str(output)
679
680 def get_rmq_cluster_running_nodes(self, sentry_unit):
681 """Parse rabbitmqctl cluster_status output string, return list of
682 running rabbitmq cluster nodes.
683
684 :param unit: sentry unit
685 :returns: List containing node names of running nodes
686 """
687 # NOTE(beisner): rabbitmqctl cluster_status output is not
688 # json-parsable, do string chop foo, then json.loads that.
689 str_stat = self.get_rmq_cluster_status(sentry_unit)
690 if 'running_nodes' in str_stat:
691 pos_start = str_stat.find("{running_nodes,") + 15
692 pos_end = str_stat.find("]},", pos_start) + 1
693 str_run_nodes = str_stat[pos_start:pos_end].replace("'", '"')
694 run_nodes = json.loads(str_run_nodes)
695 return run_nodes
696 else:
697 return []
698
699 def validate_rmq_cluster_running_nodes(self, sentry_units):
700 """Check that all rmq unit hostnames are represented in the
701 cluster_status output of all units.
702
703 :param host_names: dict of juju unit names to host names
704 :param units: list of sentry unit pointers (all rmq units)
705 :returns: None if successful, otherwise return error message
706 """
707 host_names = self.get_unit_hostnames(sentry_units)
708 errors = []
709
710 # Query every unit for cluster_status running nodes
711 for query_unit in sentry_units:
712 query_unit_name = query_unit.info['unit_name']
713 running_nodes = self.get_rmq_cluster_running_nodes(query_unit)
714
715 # Confirm that every unit is represented in the queried unit's
716 # cluster_status running nodes output.
717 for validate_unit in sentry_units:
718 val_host_name = host_names[validate_unit.info['unit_name']]
719 val_node_name = 'rabbit@{}'.format(val_host_name)
720
721 if val_node_name not in running_nodes:
722 errors.append('Cluster member check failed on {}: {} not '
723 'in {}\n'.format(query_unit_name,
724 val_node_name,
725 running_nodes))
726 if errors:
727 return ''.join(errors)
728
729 def rmq_ssl_is_enabled_on_unit(self, sentry_unit, port=None):
730 """Check a single juju rmq unit for ssl and port in the config file."""
731 host = sentry_unit.info['public-address']
732 unit_name = sentry_unit.info['unit_name']
733
734 conf_file = '/etc/rabbitmq/rabbitmq.config'
735 conf_contents = str(self.file_contents_safe(sentry_unit,
736 conf_file, max_wait=16))
737 # Checks
738 conf_ssl = 'ssl' in conf_contents
739 conf_port = str(port) in conf_contents
740
741 # Port explicitly checked in config
742 if port and conf_port and conf_ssl:
743 self.log.debug('SSL is enabled @{}:{} '
744 '({})'.format(host, port, unit_name))
745 return True
746 elif port and not conf_port and conf_ssl:
747 self.log.debug('SSL is enabled @{} but not on port {} '
748 '({})'.format(host, port, unit_name))
749 return False
750 # Port not checked (useful when checking that ssl is disabled)
751 elif not port and conf_ssl:
752 self.log.debug('SSL is enabled @{}:{} '
753 '({})'.format(host, port, unit_name))
754 return True
755 elif not port and not conf_ssl:
756 self.log.debug('SSL not enabled @{}:{} '
757 '({})'.format(host, port, unit_name))
758 return False
759 else:
760 msg = ('Unknown condition when checking SSL status @{}:{} '
761 '({})'.format(host, port, unit_name))
762 amulet.raise_status(amulet.FAIL, msg)
763
764 def validate_rmq_ssl_enabled_units(self, sentry_units, port=None):
765 """Check that ssl is enabled on rmq juju sentry units.
766
767 :param sentry_units: list of all rmq sentry units
768 :param port: optional ssl port override to validate
769 :returns: None if successful, otherwise return error message
770 """
771 for sentry_unit in sentry_units:
772 if not self.rmq_ssl_is_enabled_on_unit(sentry_unit, port=port):
773 return ('Unexpected condition: ssl is disabled on unit '
774 '({})'.format(sentry_unit.info['unit_name']))
775 return None
776
777 def validate_rmq_ssl_disabled_units(self, sentry_units):
778 """Check that ssl is enabled on listed rmq juju sentry units.
779
780 :param sentry_units: list of all rmq sentry units
781 :returns: True if successful. Raise on error.
782 """
783 for sentry_unit in sentry_units:
784 if self.rmq_ssl_is_enabled_on_unit(sentry_unit):
785 return ('Unexpected condition: ssl is enabled on unit '
786 '({})'.format(sentry_unit.info['unit_name']))
787 return None
788
789 def configure_rmq_ssl_on(self, sentry_units, deployment,
790 port=None, max_wait=60):
791 """Turn ssl charm config option on, with optional non-default
792 ssl port specification. Confirm that it is enabled on every
793 unit.
794
795 :param sentry_units: list of sentry units
796 :param deployment: amulet deployment object pointer
797 :param port: amqp port, use defaults if None
798 :param max_wait: maximum time to wait in seconds to confirm
799 :returns: None if successful. Raise on error.
800 """
801 self.log.debug('Setting ssl charm config option: on')
802
803 # Enable RMQ SSL
804 config = {'ssl': 'on'}
805 if port:
806 config['ssl_port'] = port
807
808 deployment.configure('rabbitmq-server', config)
809
810 # Confirm
811 tries = 0
812 ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
813 while ret and tries < (max_wait / 4):
814 time.sleep(4)
815 self.log.debug('Attempt {}: {}'.format(tries, ret))
816 ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
817 tries += 1
818
819 if ret:
820 amulet.raise_status(amulet.FAIL, ret)
821
822 def configure_rmq_ssl_off(self, sentry_units, deployment, max_wait=60):
823 """Turn ssl charm config option off, confirm that it is disabled
824 on every unit.
825
826 :param sentry_units: list of sentry units
827 :param deployment: amulet deployment object pointer
828 :param max_wait: maximum time to wait in seconds to confirm
829 :returns: None if successful. Raise on error.
830 """
831 self.log.debug('Setting ssl charm config option: off')
832
833 # Disable RMQ SSL
834 config = {'ssl': 'off'}
835 deployment.configure('rabbitmq-server', config)
836
837 # Confirm
838 tries = 0
839 ret = self.validate_rmq_ssl_disabled_units(sentry_units)
840 while ret and tries < (max_wait / 4):
841 time.sleep(4)
842 self.log.debug('Attempt {}: {}'.format(tries, ret))
843 ret = self.validate_rmq_ssl_disabled_units(sentry_units)
844 tries += 1
845
846 if ret:
847 amulet.raise_status(amulet.FAIL, ret)
848
849 def connect_amqp_by_unit(self, sentry_unit, ssl=False,
850 port=None, fatal=True,
851 username="testuser1", password="changeme"):
852 """Establish and return a pika amqp connection to the rabbitmq service
853 running on a rmq juju unit.
854
855 :param sentry_unit: sentry unit pointer
856 :param ssl: boolean, default to False
857 :param port: amqp port, use defaults if None
858 :param fatal: boolean, default to True (raises on connect error)
859 :param username: amqp user name, default to testuser1
860 :param password: amqp user password
861 :returns: pika amqp connection pointer or None if failed and non-fatal
862 """
863 host = sentry_unit.info['public-address']
864 unit_name = sentry_unit.info['unit_name']
865
866 # Default port logic if port is not specified
867 if ssl and not port:
868 port = 5671
869 elif not ssl and not port:
870 port = 5672
871
872 self.log.debug('Connecting to amqp on {}:{} ({}) as '
873 '{}...'.format(host, port, unit_name, username))
874
875 try:
876 credentials = pika.PlainCredentials(username, password)
877 parameters = pika.ConnectionParameters(host=host, port=port,
878 credentials=credentials,
879 ssl=ssl,
880 connection_attempts=3,
881 retry_delay=5,
882 socket_timeout=1)
883 connection = pika.BlockingConnection(parameters)
884 assert connection.server_properties['product'] == 'RabbitMQ'
885 self.log.debug('Connect OK')
886 return connection
887 except Exception as e:
888 msg = ('amqp connection failed to {}:{} as '
889 '{} ({})'.format(host, port, username, str(e)))
890 if fatal:
891 amulet.raise_status(amulet.FAIL, msg)
892 else:
893 self.log.warn(msg)
894 return None
895
896 def publish_amqp_message_by_unit(self, sentry_unit, message,
897 queue="test", ssl=False,
898 username="testuser1",
899 password="changeme",
900 port=None):
901 """Publish an amqp message to a rmq juju unit.
902
903 :param sentry_unit: sentry unit pointer
904 :param message: amqp message string
905 :param queue: message queue, default to test
906 :param username: amqp user name, default to testuser1
907 :param password: amqp user password
908 :param ssl: boolean, default to False
909 :param port: amqp port, use defaults if None
910 :returns: None. Raises exception if publish failed.
911 """
912 self.log.debug('Publishing message to {} queue:\n{}'.format(queue,
913 message))
914 connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
915 port=port,
916 username=username,
917 password=password)
918
919 # NOTE(beisner): extra debug here re: pika hang potential:
920 # https://github.com/pika/pika/issues/297
921 # https://groups.google.com/forum/#!topic/rabbitmq-users/Ja0iyfF0Szw
922 self.log.debug('Defining channel...')
923 channel = connection.channel()
924 self.log.debug('Declaring queue...')
925 channel.queue_declare(queue=queue, auto_delete=False, durable=True)
926 self.log.debug('Publishing message...')
927 channel.basic_publish(exchange='', routing_key=queue, body=message)
928 self.log.debug('Closing channel...')
929 channel.close()
930 self.log.debug('Closing connection...')
931 connection.close()
932
933 def get_amqp_message_by_unit(self, sentry_unit, queue="test",
934 username="testuser1",
935 password="changeme",
936 ssl=False, port=None):
937 """Get an amqp message from a rmq juju unit.
938
939 :param sentry_unit: sentry unit pointer
940 :param queue: message queue, default to test
941 :param username: amqp user name, default to testuser1
942 :param password: amqp user password
943 :param ssl: boolean, default to False
944 :param port: amqp port, use defaults if None
945 :returns: amqp message body as string. Raise if get fails.
946 """
947 connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
948 port=port,
949 username=username,
950 password=password)
951 channel = connection.channel()
952 method_frame, _, body = channel.basic_get(queue)
953
954 if method_frame:
955 self.log.debug('Retreived message from {} queue:\n{}'.format(queue,
956 body))
957 channel.basic_ack(method_frame.delivery_tag)
958 channel.close()
959 connection.close()
960 return body
961 else:
962 msg = 'No message retrieved.'
963 amulet.raise_status(amulet.FAIL, msg)
605964
=== modified file 'charmhelpers/contrib/openstack/context.py'
--- charmhelpers/contrib/openstack/context.py 2015-09-03 09:40:28 +0000
+++ charmhelpers/contrib/openstack/context.py 2015-09-25 14:42:33 +0000
@@ -194,10 +194,50 @@
194class OSContextGenerator(object):194class OSContextGenerator(object):
195 """Base class for all context generators."""195 """Base class for all context generators."""
196 interfaces = []196 interfaces = []
197 related = False
198 complete = False
199 missing_data = []
197200
198 def __call__(self):201 def __call__(self):
199 raise NotImplementedError202 raise NotImplementedError
200203
204 def context_complete(self, ctxt):
205 """Check for missing data for the required context data.
206 Set self.missing_data if it exists and return False.
207 Set self.complete if no missing data and return True.
208 """
209 # Fresh start
210 self.complete = False
211 self.missing_data = []
212 for k, v in six.iteritems(ctxt):
213 if v is None or v == '':
214 if k not in self.missing_data:
215 self.missing_data.append(k)
216
217 if self.missing_data:
218 self.complete = False
219 log('Missing required data: %s' % ' '.join(self.missing_data), level=INFO)
220 else:
221 self.complete = True
222 return self.complete
223
224 def get_related(self):
225 """Check if any of the context interfaces have relation ids.
226 Set self.related and return True if one of the interfaces
227 has relation ids.
228 """
229 # Fresh start
230 self.related = False
231 try:
232 for interface in self.interfaces:
233 if relation_ids(interface):
234 self.related = True
235 return self.related
236 except AttributeError as e:
237 log("{} {}"
238 "".format(self, e), 'INFO')
239 return self.related
240
201241
202class SharedDBContext(OSContextGenerator):242class SharedDBContext(OSContextGenerator):
203 interfaces = ['shared-db']243 interfaces = ['shared-db']
@@ -213,6 +253,7 @@
213 self.database = database253 self.database = database
214 self.user = user254 self.user = user
215 self.ssl_dir = ssl_dir255 self.ssl_dir = ssl_dir
256 self.rel_name = self.interfaces[0]
216257
217 def __call__(self):258 def __call__(self):
218 self.database = self.database or config('database')259 self.database = self.database or config('database')
@@ -246,6 +287,7 @@
246 password_setting = self.relation_prefix + '_password'287 password_setting = self.relation_prefix + '_password'
247288
248 for rid in relation_ids(self.interfaces[0]):289 for rid in relation_ids(self.interfaces[0]):
290 self.related = True
249 for unit in related_units(rid):291 for unit in related_units(rid):
250 rdata = relation_get(rid=rid, unit=unit)292 rdata = relation_get(rid=rid, unit=unit)
251 host = rdata.get('db_host')293 host = rdata.get('db_host')
@@ -257,7 +299,7 @@
257 'database_password': rdata.get(password_setting),299 'database_password': rdata.get(password_setting),
258 'database_type': 'mysql'300 'database_type': 'mysql'
259 }301 }
260 if context_complete(ctxt):302 if self.context_complete(ctxt):
261 db_ssl(rdata, ctxt, self.ssl_dir)303 db_ssl(rdata, ctxt, self.ssl_dir)
262 return ctxt304 return ctxt
263 return {}305 return {}
@@ -278,6 +320,7 @@
278320
279 ctxt = {}321 ctxt = {}
280 for rid in relation_ids(self.interfaces[0]):322 for rid in relation_ids(self.interfaces[0]):
323 self.related = True
281 for unit in related_units(rid):324 for unit in related_units(rid):
282 rel_host = relation_get('host', rid=rid, unit=unit)325 rel_host = relation_get('host', rid=rid, unit=unit)
283 rel_user = relation_get('user', rid=rid, unit=unit)326 rel_user = relation_get('user', rid=rid, unit=unit)
@@ -287,7 +330,7 @@
287 'database_user': rel_user,330 'database_user': rel_user,
288 'database_password': rel_passwd,331 'database_password': rel_passwd,
289 'database_type': 'postgresql'}332 'database_type': 'postgresql'}
290 if context_complete(ctxt):333 if self.context_complete(ctxt):
291 return ctxt334 return ctxt
292335
293 return {}336 return {}
@@ -348,6 +391,7 @@
348 ctxt['signing_dir'] = cachedir391 ctxt['signing_dir'] = cachedir
349392
350 for rid in relation_ids(self.rel_name):393 for rid in relation_ids(self.rel_name):
394 self.related = True
351 for unit in related_units(rid):395 for unit in related_units(rid):
352 rdata = relation_get(rid=rid, unit=unit)396 rdata = relation_get(rid=rid, unit=unit)
353 serv_host = rdata.get('service_host')397 serv_host = rdata.get('service_host')
@@ -366,7 +410,7 @@
366 'service_protocol': svc_protocol,410 'service_protocol': svc_protocol,
367 'auth_protocol': auth_protocol})411 'auth_protocol': auth_protocol})
368412
369 if context_complete(ctxt):413 if self.context_complete(ctxt):
370 # NOTE(jamespage) this is required for >= icehouse414 # NOTE(jamespage) this is required for >= icehouse
371 # so a missing value just indicates keystone needs415 # so a missing value just indicates keystone needs
372 # upgrading416 # upgrading
@@ -405,6 +449,7 @@
405 ctxt = {}449 ctxt = {}
406 for rid in relation_ids(self.rel_name):450 for rid in relation_ids(self.rel_name):
407 ha_vip_only = False451 ha_vip_only = False
452 self.related = True
408 for unit in related_units(rid):453 for unit in related_units(rid):
409 if relation_get('clustered', rid=rid, unit=unit):454 if relation_get('clustered', rid=rid, unit=unit):
410 ctxt['clustered'] = True455 ctxt['clustered'] = True
@@ -437,7 +482,7 @@
437 ha_vip_only = relation_get('ha-vip-only',482 ha_vip_only = relation_get('ha-vip-only',
438 rid=rid, unit=unit) is not None483 rid=rid, unit=unit) is not None
439484
440 if context_complete(ctxt):485 if self.context_complete(ctxt):
441 if 'rabbit_ssl_ca' in ctxt:486 if 'rabbit_ssl_ca' in ctxt:
442 if not self.ssl_dir:487 if not self.ssl_dir:
443 log("Charm not setup for ssl support but ssl ca "488 log("Charm not setup for ssl support but ssl ca "
@@ -469,7 +514,7 @@
469 ctxt['oslo_messaging_flags'] = config_flags_parser(514 ctxt['oslo_messaging_flags'] = config_flags_parser(
470 oslo_messaging_flags)515 oslo_messaging_flags)
471516
472 if not context_complete(ctxt):517 if not self.complete:
473 return {}518 return {}
474519
475 return ctxt520 return ctxt
@@ -485,13 +530,15 @@
485530
486 log('Generating template context for ceph', level=DEBUG)531 log('Generating template context for ceph', level=DEBUG)
487 mon_hosts = []532 mon_hosts = []
488 auth = None533 ctxt = {
489 key = None534 'use_syslog': str(config('use-syslog')).lower()
490 use_syslog = str(config('use-syslog')).lower()535 }
491 for rid in relation_ids('ceph'):536 for rid in relation_ids('ceph'):
492 for unit in related_units(rid):537 for unit in related_units(rid):
493 auth = relation_get('auth', rid=rid, unit=unit)538 if not ctxt.get('auth'):
494 key = relation_get('key', rid=rid, unit=unit)539 ctxt['auth'] = relation_get('auth', rid=rid, unit=unit)
540 if not ctxt.get('key'):
541 ctxt['key'] = relation_get('key', rid=rid, unit=unit)
495 ceph_pub_addr = relation_get('ceph-public-address', rid=rid,542 ceph_pub_addr = relation_get('ceph-public-address', rid=rid,
496 unit=unit)543 unit=unit)
497 unit_priv_addr = relation_get('private-address', rid=rid,544 unit_priv_addr = relation_get('private-address', rid=rid,
@@ -500,15 +547,12 @@
500 ceph_addr = format_ipv6_addr(ceph_addr) or ceph_addr547 ceph_addr = format_ipv6_addr(ceph_addr) or ceph_addr
501 mon_hosts.append(ceph_addr)548 mon_hosts.append(ceph_addr)
502549
503 ctxt = {'mon_hosts': ' '.join(sorted(mon_hosts)),550 ctxt['mon_hosts'] = ' '.join(sorted(mon_hosts))
504 'auth': auth,
505 'key': key,
506 'use_syslog': use_syslog}
507551
508 if not os.path.isdir('/etc/ceph'):552 if not os.path.isdir('/etc/ceph'):
509 os.mkdir('/etc/ceph')553 os.mkdir('/etc/ceph')
510554
511 if not context_complete(ctxt):555 if not self.context_complete(ctxt):
512 return {}556 return {}
513557
514 ensure_packages(['ceph-common'])558 ensure_packages(['ceph-common'])
@@ -1367,6 +1411,6 @@
1367 'auth_protocol':1411 'auth_protocol':
1368 rdata.get('auth_protocol') or 'http',1412 rdata.get('auth_protocol') or 'http',
1369 }1413 }
1370 if context_complete(ctxt):1414 if self.context_complete(ctxt):
1371 return ctxt1415 return ctxt
1372 return {}1416 return {}
13731417
=== modified file 'charmhelpers/contrib/openstack/templating.py'
--- charmhelpers/contrib/openstack/templating.py 2015-07-29 10:47:17 +0000
+++ charmhelpers/contrib/openstack/templating.py 2015-09-25 14:42:33 +0000
@@ -18,7 +18,7 @@
1818
19import six19import six
2020
21from charmhelpers.fetch import apt_install21from charmhelpers.fetch import apt_install, apt_update
22from charmhelpers.core.hookenv import (22from charmhelpers.core.hookenv import (
23 log,23 log,
24 ERROR,24 ERROR,
@@ -29,6 +29,7 @@
29try:29try:
30 from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions30 from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
31except ImportError:31except ImportError:
32 apt_update(fatal=True)
32 apt_install('python-jinja2', fatal=True)33 apt_install('python-jinja2', fatal=True)
33 from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions34 from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
3435
@@ -112,7 +113,7 @@
112113
113 def complete_contexts(self):114 def complete_contexts(self):
114 '''115 '''
115 Return a list of interfaces that have atisfied contexts.116 Return a list of interfaces that have satisfied contexts.
116 '''117 '''
117 if self._complete_contexts:118 if self._complete_contexts:
118 return self._complete_contexts119 return self._complete_contexts
@@ -293,3 +294,30 @@
293 [interfaces.extend(i.complete_contexts())294 [interfaces.extend(i.complete_contexts())
294 for i in six.itervalues(self.templates)]295 for i in six.itervalues(self.templates)]
295 return interfaces296 return interfaces
297
298 def get_incomplete_context_data(self, interfaces):
299 '''
300 Return dictionary of relation status of interfaces and any missing
301 required context data. Example:
302 {'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
303 'zeromq-configuration': {'related': False}}
304 '''
305 incomplete_context_data = {}
306
307 for i in six.itervalues(self.templates):
308 for context in i.contexts:
309 for interface in interfaces:
310 related = False
311 if interface in context.interfaces:
312 related = context.get_related()
313 missing_data = context.missing_data
314 if missing_data:
315 incomplete_context_data[interface] = {'missing_data': missing_data}
316 if related:
317 if incomplete_context_data.get(interface):
318 incomplete_context_data[interface].update({'related': True})
319 else:
320 incomplete_context_data[interface] = {'related': True}
321 else:
322 incomplete_context_data[interface] = {'related': False}
323 return incomplete_context_data
296324
=== modified file 'charmhelpers/contrib/openstack/utils.py'
--- charmhelpers/contrib/openstack/utils.py 2015-09-03 09:40:28 +0000
+++ charmhelpers/contrib/openstack/utils.py 2015-09-25 14:42:33 +0000
@@ -25,6 +25,7 @@
25import re25import re
2626
27import six27import six
28import traceback
28import yaml29import yaml
2930
30from charmhelpers.contrib.network import ip31from charmhelpers.contrib.network import ip
@@ -34,12 +35,16 @@
34)35)
3536
36from charmhelpers.core.hookenv import (37from charmhelpers.core.hookenv import (
38 action_fail,
39 action_set,
37 config,40 config,
38 log as juju_log,41 log as juju_log,
39 charm_dir,42 charm_dir,
40 INFO,43 INFO,
41 relation_ids,44 relation_ids,
42 relation_set45 relation_set,
46 status_set,
47 hook_name
43)48)
4449
45from charmhelpers.contrib.storage.linux.lvm import (50from charmhelpers.contrib.storage.linux.lvm import (
@@ -49,7 +54,8 @@
49)54)
5055
51from charmhelpers.contrib.network.ip import (56from charmhelpers.contrib.network.ip import (
52 get_ipv6_addr57 get_ipv6_addr,
58 is_ipv6,
53)59)
5460
55from charmhelpers.contrib.python.packages import (61from charmhelpers.contrib.python.packages import (
@@ -114,6 +120,7 @@
114 ('2.2.1', 'kilo'),120 ('2.2.1', 'kilo'),
115 ('2.2.2', 'kilo'),121 ('2.2.2', 'kilo'),
116 ('2.3.0', 'liberty'),122 ('2.3.0', 'liberty'),
123 ('2.4.0', 'liberty'),
117])124])
118125
119# >= Liberty version->codename mapping126# >= Liberty version->codename mapping
@@ -142,6 +149,9 @@
142 'glance-common': OrderedDict([149 'glance-common': OrderedDict([
143 ('11.0.0', 'liberty'),150 ('11.0.0', 'liberty'),
144 ]),151 ]),
152 'openstack-dashboard': OrderedDict([
153 ('8.0.0', 'liberty'),
154 ]),
145}155}
146156
147DEFAULT_LOOPBACK_SIZE = '5G'157DEFAULT_LOOPBACK_SIZE = '5G'
@@ -510,6 +520,12 @@
510 relation_prefix=None):520 relation_prefix=None):
511 hosts = get_ipv6_addr(dynamic_only=False)521 hosts = get_ipv6_addr(dynamic_only=False)
512522
523 if config('vip'):
524 vips = config('vip').split()
525 for vip in vips:
526 if vip and is_ipv6(vip):
527 hosts.append(vip)
528
513 kwargs = {'database': database,529 kwargs = {'database': database,
514 'username': database_user,530 'username': database_user,
515 'hostname': json.dumps(hosts)}531 'hostname': json.dumps(hosts)}
@@ -745,3 +761,217 @@
745 return projects[key]761 return projects[key]
746762
747 return None763 return None
764
765
766def os_workload_status(configs, required_interfaces, charm_func=None):
767 """
768 Decorator to set workload status based on complete contexts
769 """
770 def wrap(f):
771 @wraps(f)
772 def wrapped_f(*args, **kwargs):
773 # Run the original function first
774 f(*args, **kwargs)
775 # Set workload status now that contexts have been
776 # acted on
777 set_os_workload_status(configs, required_interfaces, charm_func)
778 return wrapped_f
779 return wrap
780
781
782def set_os_workload_status(configs, required_interfaces, charm_func=None):
783 """
784 Set workload status based on complete contexts.
785 status-set missing or incomplete contexts
786 and juju-log details of missing required data.
787 charm_func is a charm specific function to run checking
788 for charm specific requirements such as a VIP setting.
789 """
790 incomplete_rel_data = incomplete_relation_data(configs, required_interfaces)
791 state = 'active'
792 missing_relations = []
793 incomplete_relations = []
794 message = None
795 charm_state = None
796 charm_message = None
797
798 for generic_interface in incomplete_rel_data.keys():
799 related_interface = None
800 missing_data = {}
801 # Related or not?
802 for interface in incomplete_rel_data[generic_interface]:
803 if incomplete_rel_data[generic_interface][interface].get('related'):
804 related_interface = interface
805 missing_data = incomplete_rel_data[generic_interface][interface].get('missing_data')
806 # No relation ID for the generic_interface
807 if not related_interface:
808 juju_log("{} relation is missing and must be related for "
809 "functionality. ".format(generic_interface), 'WARN')
810 state = 'blocked'
811 if generic_interface not in missing_relations:
812 missing_relations.append(generic_interface)
813 else:
814 # Relation ID exists but no related unit
815 if not missing_data:
816 # Edge case relation ID exists but departing
817 if ('departed' in hook_name() or 'broken' in hook_name()) \
818 and related_interface in hook_name():
819 state = 'blocked'
820 if generic_interface not in missing_relations:
821 missing_relations.append(generic_interface)
822 juju_log("{} relation's interface, {}, "
823 "relationship is departed or broken "
824 "and is required for functionality."
825 "".format(generic_interface, related_interface), "WARN")
826 # Normal case relation ID exists but no related unit
827 # (joining)
828 else:
829 juju_log("{} relations's interface, {}, is related but has "
830 "no units in the relation."
831 "".format(generic_interface, related_interface), "INFO")
832 # Related unit exists and data missing on the relation
833 else:
834 juju_log("{} relation's interface, {}, is related awaiting "
835 "the following data from the relationship: {}. "
836 "".format(generic_interface, related_interface,
837 ", ".join(missing_data)), "INFO")
838 if state != 'blocked':
839 state = 'waiting'
840 if generic_interface not in incomplete_relations \
841 and generic_interface not in missing_relations:
842 incomplete_relations.append(generic_interface)
843
844 if missing_relations:
845 message = "Missing relations: {}".format(", ".join(missing_relations))
846 if incomplete_relations:
847 message += "; incomplete relations: {}" \
848 "".format(", ".join(incomplete_relations))
849 state = 'blocked'
850 elif incomplete_relations:
851 message = "Incomplete relations: {}" \
852 "".format(", ".join(incomplete_relations))
853 state = 'waiting'
854
855 # Run charm specific checks
856 if charm_func:
857 charm_state, charm_message = charm_func(configs)
858 if charm_state != 'active' and charm_state != 'unknown':
859 state = workload_state_compare(state, charm_state)
860 if message:
861 message = "{} {}".format(message, charm_message)
862 else:
863 message = charm_message
864
865 # Set to active if all requirements have been met
866 if state == 'active':
867 message = "Unit is ready"
868 juju_log(message, "INFO")
869
870 status_set(state, message)
871
872
873def workload_state_compare(current_workload_state, workload_state):
874 """ Return highest priority of two states"""
875 hierarchy = {'unknown': -1,
876 'active': 0,
877 'maintenance': 1,
878 'waiting': 2,
879 'blocked': 3,
880 }
881
882 if hierarchy.get(workload_state) is None:
883 workload_state = 'unknown'
884 if hierarchy.get(current_workload_state) is None:
885 current_workload_state = 'unknown'
886
887 # Set workload_state based on hierarchy of statuses
888 if hierarchy.get(current_workload_state) > hierarchy.get(workload_state):
889 return current_workload_state
890 else:
891 return workload_state
892
893
894def incomplete_relation_data(configs, required_interfaces):
895 """
896 Check complete contexts against required_interfaces
897 Return dictionary of incomplete relation data.
898
899 configs is an OSConfigRenderer object with configs registered
900
901 required_interfaces is a dictionary of required general interfaces
902 with dictionary values of possible specific interfaces.
903 Example:
904 required_interfaces = {'database': ['shared-db', 'pgsql-db']}
905
906 The interface is said to be satisfied if anyone of the interfaces in the
907 list has a complete context.
908
909 Return dictionary of incomplete or missing required contexts with relation
910 status of interfaces and any missing data points. Example:
911 {'message':
912 {'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
913 'zeromq-configuration': {'related': False}},
914 'identity':
915 {'identity-service': {'related': False}},
916 'database':
917 {'pgsql-db': {'related': False},
918 'shared-db': {'related': True}}}
919 """
920 complete_ctxts = configs.complete_contexts()
921 incomplete_relations = []
922 for svc_type in required_interfaces.keys():
923 # Avoid duplicates
924 found_ctxt = False
925 for interface in required_interfaces[svc_type]:
926 if interface in complete_ctxts:
927 found_ctxt = True
928 if not found_ctxt:
929 incomplete_relations.append(svc_type)
930 incomplete_context_data = {}
931 for i in incomplete_relations:
932 incomplete_context_data[i] = configs.get_incomplete_context_data(required_interfaces[i])
933 return incomplete_context_data
934
935
936def do_action_openstack_upgrade(package, upgrade_callback, configs):
937 """Perform action-managed OpenStack upgrade.
938
939 Upgrades packages to the configured openstack-origin version and sets
940 the corresponding action status as a result.
941
942 If the charm was installed from source we cannot upgrade it.
943 For backwards compatibility a config flag (action-managed-upgrade) must
944 be set for this code to run, otherwise a full service level upgrade will
945 fire on config-changed.
946
947 @param package: package name for determining if upgrade available
948 @param upgrade_callback: function callback to charm's upgrade function
949 @param configs: templating object derived from OSConfigRenderer class
950
951 @return: True if upgrade successful; False if upgrade failed or skipped
952 """
953 ret = False
954
955 if git_install_requested():
956 action_set({'outcome': 'installed from source, skipped upgrade.'})
957 else:
958 if openstack_upgrade_available(package):
959 if config('action-managed-upgrade'):
960 juju_log('Upgrading OpenStack release')
961
962 try:
963 upgrade_callback(configs=configs)
964 action_set({'outcome': 'success, upgrade completed.'})
965 ret = True
966 except:
967 action_set({'outcome': 'upgrade failed, see traceback.'})
968 action_set({'traceback': traceback.format_exc()})
969 action_fail('do_openstack_upgrade resulted in an '
970 'unexpected error')
971 else:
972 action_set({'outcome': 'action-managed-upgrade config is '
973 'False, skipped upgrade.'})
974 else:
975 action_set({'outcome': 'no upgrade available.'})
976
977 return ret
748978
=== modified file 'charmhelpers/contrib/storage/linux/ceph.py'
--- charmhelpers/contrib/storage/linux/ceph.py 2015-07-16 20:17:38 +0000
+++ charmhelpers/contrib/storage/linux/ceph.py 2015-09-25 14:42:33 +0000
@@ -28,6 +28,7 @@
28import shutil28import shutil
29import json29import json
30import time30import time
31import uuid
3132
32from subprocess import (33from subprocess import (
33 check_call,34 check_call,
@@ -35,8 +36,10 @@
35 CalledProcessError,36 CalledProcessError,
36)37)
37from charmhelpers.core.hookenv import (38from charmhelpers.core.hookenv import (
39 local_unit,
38 relation_get,40 relation_get,
39 relation_ids,41 relation_ids,
42 relation_set,
40 related_units,43 related_units,
41 log,44 log,
42 DEBUG,45 DEBUG,
@@ -56,6 +59,8 @@
56 apt_install,59 apt_install,
57)60)
5861
62from charmhelpers.core.kernel import modprobe
63
59KEYRING = '/etc/ceph/ceph.client.{}.keyring'64KEYRING = '/etc/ceph/ceph.client.{}.keyring'
60KEYFILE = '/etc/ceph/ceph.client.{}.key'65KEYFILE = '/etc/ceph/ceph.client.{}.key'
6166
@@ -288,17 +293,6 @@
288 os.chown(data_src_dst, uid, gid)293 os.chown(data_src_dst, uid, gid)
289294
290295
291# TODO: re-use
292def modprobe(module):
293 """Load a kernel module and configure for auto-load on reboot."""
294 log('Loading kernel module', level=INFO)
295 cmd = ['modprobe', module]
296 check_call(cmd)
297 with open('/etc/modules', 'r+') as modules:
298 if module not in modules.read():
299 modules.write(module)
300
301
302def copy_files(src, dst, symlinks=False, ignore=None):296def copy_files(src, dst, symlinks=False, ignore=None):
303 """Copy files from src to dst."""297 """Copy files from src to dst."""
304 for item in os.listdir(src):298 for item in os.listdir(src):
@@ -411,17 +405,52 @@
411405
412 The API is versioned and defaults to version 1.406 The API is versioned and defaults to version 1.
413 """407 """
414 def __init__(self, api_version=1):408 def __init__(self, api_version=1, request_id=None):
415 self.api_version = api_version409 self.api_version = api_version
410 if request_id:
411 self.request_id = request_id
412 else:
413 self.request_id = str(uuid.uuid1())
416 self.ops = []414 self.ops = []
417415
418 def add_op_create_pool(self, name, replica_count=3):416 def add_op_create_pool(self, name, replica_count=3):
419 self.ops.append({'op': 'create-pool', 'name': name,417 self.ops.append({'op': 'create-pool', 'name': name,
420 'replicas': replica_count})418 'replicas': replica_count})
421419
420 def set_ops(self, ops):
421 """Set request ops to provided value.
422
423 Useful for injecting ops that come from a previous request
424 to allow comparisons to ensure validity.
425 """
426 self.ops = ops
427
422 @property428 @property
423 def request(self):429 def request(self):
424 return json.dumps({'api-version': self.api_version, 'ops': self.ops})430 return json.dumps({'api-version': self.api_version, 'ops': self.ops,
431 'request-id': self.request_id})
432
433 def _ops_equal(self, other):
434 if len(self.ops) == len(other.ops):
435 for req_no in range(0, len(self.ops)):
436 for key in ['replicas', 'name', 'op']:
437 if self.ops[req_no][key] != other.ops[req_no][key]:
438 return False
439 else:
440 return False
441 return True
442
443 def __eq__(self, other):
444 if not isinstance(other, self.__class__):
445 return False
446 if self.api_version == other.api_version and \
447 self._ops_equal(other):
448 return True
449 else:
450 return False
451
452 def __ne__(self, other):
453 return not self.__eq__(other)
425454
426455
427class CephBrokerRsp(object):456class CephBrokerRsp(object):
@@ -431,14 +460,198 @@
431460
432 The API is versioned and defaults to version 1.461 The API is versioned and defaults to version 1.
433 """462 """
463
434 def __init__(self, encoded_rsp):464 def __init__(self, encoded_rsp):
435 self.api_version = None465 self.api_version = None
436 self.rsp = json.loads(encoded_rsp)466 self.rsp = json.loads(encoded_rsp)
437467
438 @property468 @property
469 def request_id(self):
470 return self.rsp.get('request-id')
471
472 @property
439 def exit_code(self):473 def exit_code(self):
440 return self.rsp.get('exit-code')474 return self.rsp.get('exit-code')
441475
442 @property476 @property
443 def exit_msg(self):477 def exit_msg(self):
444 return self.rsp.get('stderr')478 return self.rsp.get('stderr')
479
480
481# Ceph Broker Conversation:
482# If a charm needs an action to be taken by ceph it can create a CephBrokerRq
483# and send that request to ceph via the ceph relation. The CephBrokerRq has a
484# unique id so that the client can identity which CephBrokerRsp is associated
485# with the request. Ceph will also respond to each client unit individually
486# creating a response key per client unit eg glance/0 will get a CephBrokerRsp
487# via key broker-rsp-glance-0
488#
489# To use this the charm can just do something like:
490#
491# from charmhelpers.contrib.storage.linux.ceph import (
492# send_request_if_needed,
493# is_request_complete,
494# CephBrokerRq,
495# )
496#
497# @hooks.hook('ceph-relation-changed')
498# def ceph_changed():
499# rq = CephBrokerRq()
500# rq.add_op_create_pool(name='poolname', replica_count=3)
501#
502# if is_request_complete(rq):
503# <Request complete actions>
504# else:
505# send_request_if_needed(get_ceph_request())
506#
507# CephBrokerRq and CephBrokerRsp are serialized into JSON. Below is an example
508# of glance having sent a request to ceph which ceph has successfully processed
509# 'ceph:8': {
510# 'ceph/0': {
511# 'auth': 'cephx',
512# 'broker-rsp-glance-0': '{"request-id": "0bc7dc54", "exit-code": 0}',
513# 'broker_rsp': '{"request-id": "0da543b8", "exit-code": 0}',
514# 'ceph-public-address': '10.5.44.103',
515# 'key': 'AQCLDttVuHXINhAAvI144CB09dYchhHyTUY9BQ==',
516# 'private-address': '10.5.44.103',
517# },
518# 'glance/0': {
519# 'broker_req': ('{"api-version": 1, "request-id": "0bc7dc54", '
520# '"ops": [{"replicas": 3, "name": "glance", '
521# '"op": "create-pool"}]}'),
522# 'private-address': '10.5.44.109',
523# },
524# }
525
526def get_previous_request(rid):
527 """Return the last ceph broker request sent on a given relation
528
529 @param rid: Relation id to query for request
530 """
531 request = None
532 broker_req = relation_get(attribute='broker_req', rid=rid,
533 unit=local_unit())
534 if broker_req:
535 request_data = json.loads(broker_req)
536 request = CephBrokerRq(api_version=request_data['api-version'],
537 request_id=request_data['request-id'])
538 request.set_ops(request_data['ops'])
539
540 return request
541
542
543def get_request_states(request):
544 """Return a dict of requests per relation id with their corresponding
545 completion state.
546
547 This allows a charm, which has a request for ceph, to see whether there is
548 an equivalent request already being processed and if so what state that
549 request is in.
550
551 @param request: A CephBrokerRq object
552 """
553 complete = []
554 requests = {}
555 for rid in relation_ids('ceph'):
556 complete = False
557 previous_request = get_previous_request(rid)
558 if request == previous_request:
559 sent = True
560 complete = is_request_complete_for_rid(previous_request, rid)
561 else:
562 sent = False
563 complete = False
564
565 requests[rid] = {
566 'sent': sent,
567 'complete': complete,
568 }
569
570 return requests
571
572
573def is_request_sent(request):
574 """Check to see if a functionally equivalent request has already been sent
575
576 Returns True if a similair request has been sent
577
578 @param request: A CephBrokerRq object
579 """
580 states = get_request_states(request)
581 for rid in states.keys():
582 if not states[rid]['sent']:
583 return False
584
585 return True
586
587
588def is_request_complete(request):
589 """Check to see if a functionally equivalent request has already been
590 completed
591
592 Returns True if a similair request has been completed
593
594 @param request: A CephBrokerRq object
595 """
596 states = get_request_states(request)
597 for rid in states.keys():
598 if not states[rid]['complete']:
599 return False
600
601 return True
602
603
604def is_request_complete_for_rid(request, rid):
605 """Check if a given request has been completed on the given relation
606
607 @param request: A CephBrokerRq object
608 @param rid: Relation ID
609 """
610 broker_key = get_broker_rsp_key()
611 for unit in related_units(rid):
612 rdata = relation_get(rid=rid, unit=unit)
613 if rdata.get(broker_key):
614 rsp = CephBrokerRsp(rdata.get(broker_key))
615 if rsp.request_id == request.request_id:
616 if not rsp.exit_code:
617 return True
618 else:
619 # The remote unit sent no reply targeted at this unit so either the
620 # remote ceph cluster does not support unit targeted replies or it
621 # has not processed our request yet.
622 if rdata.get('broker_rsp'):
623 request_data = json.loads(rdata['broker_rsp'])
624 if request_data.get('request-id'):
625 log('Ignoring legacy broker_rsp without unit key as remote '
626 'service supports unit specific replies', level=DEBUG)
627 else:
628 log('Using legacy broker_rsp as remote service does not '
629 'supports unit specific replies', level=DEBUG)
630 rsp = CephBrokerRsp(rdata['broker_rsp'])
631 if not rsp.exit_code:
632 return True
633
634 return False
635
636
637def get_broker_rsp_key():
638 """Return broker response key for this unit
639
640 This is the key that ceph is going to use to pass request status
641 information back to this unit
642 """
643 return 'broker-rsp-' + local_unit().replace('/', '-')
644
645
646def send_request_if_needed(request):
647 """Send broker request if an equivalent request has not already been sent
648
649 @param request: A CephBrokerRq object
650 """
651 if is_request_sent(request):
652 log('Request already sent but not complete, not sending new request',
653 level=DEBUG)
654 else:
655 for rid in relation_ids('ceph'):
656 log('Sending request {}'.format(request.request_id), level=DEBUG)
657 relation_set(relation_id=rid, broker_req=request.request)
445658
=== modified file 'charmhelpers/core/hookenv.py'
--- charmhelpers/core/hookenv.py 2015-09-03 09:40:28 +0000
+++ charmhelpers/core/hookenv.py 2015-09-25 14:42:33 +0000
@@ -623,6 +623,38 @@
623 return unit_get('private-address')623 return unit_get('private-address')
624624
625625
626@cached
627def storage_get(attribute="", storage_id=""):
628 """Get storage attributes"""
629 _args = ['storage-get', '--format=json']
630 if storage_id:
631 _args.extend(('-s', storage_id))
632 if attribute:
633 _args.append(attribute)
634 try:
635 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
636 except ValueError:
637 return None
638
639
640@cached
641def storage_list(storage_name=""):
642 """List the storage IDs for the unit"""
643 _args = ['storage-list', '--format=json']
644 if storage_name:
645 _args.append(storage_name)
646 try:
647 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
648 except ValueError:
649 return None
650 except OSError as e:
651 import errno
652 if e.errno == errno.ENOENT:
653 # storage-list does not exist
654 return []
655 raise
656
657
626class UnregisteredHookError(Exception):658class UnregisteredHookError(Exception):
627 """Raised when an undefined hook is called"""659 """Raised when an undefined hook is called"""
628 pass660 pass
629661
=== modified file 'charmhelpers/core/host.py'
--- charmhelpers/core/host.py 2015-08-24 08:56:12 +0000
+++ charmhelpers/core/host.py 2015-09-25 14:42:33 +0000
@@ -63,32 +63,48 @@
63 return service_result63 return service_result
6464
6565
66def service_pause(service_name, init_dir=None):66def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
67 """Pause a system service.67 """Pause a system service.
6868
69 Stop it, and prevent it from starting again at boot."""69 Stop it, and prevent it from starting again at boot."""
70 if init_dir is None:
71 init_dir = "/etc/init"
72 stopped = service_stop(service_name)70 stopped = service_stop(service_name)
73 # XXX: Support systemd too71 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
74 override_path = os.path.join(72 sysv_file = os.path.join(initd_dir, service_name)
75 init_dir, '{}.override'.format(service_name))73 if os.path.exists(upstart_file):
76 with open(override_path, 'w') as fh:74 override_path = os.path.join(
77 fh.write("manual\n")75 init_dir, '{}.override'.format(service_name))
76 with open(override_path, 'w') as fh:
77 fh.write("manual\n")
78 elif os.path.exists(sysv_file):
79 subprocess.check_call(["update-rc.d", service_name, "disable"])
80 else:
81 # XXX: Support SystemD too
82 raise ValueError(
83 "Unable to detect {0} as either Upstart {1} or SysV {2}".format(
84 service_name, upstart_file, sysv_file))
78 return stopped85 return stopped
7986
8087
81def service_resume(service_name, init_dir=None):88def service_resume(service_name, init_dir="/etc/init",
89 initd_dir="/etc/init.d"):
82 """Resume a system service.90 """Resume a system service.
8391
84 Reenable starting again at boot. Start the service"""92 Reenable starting again at boot. Start the service"""
85 # XXX: Support systemd too93 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
86 if init_dir is None:94 sysv_file = os.path.join(initd_dir, service_name)
87 init_dir = "/etc/init"95 if os.path.exists(upstart_file):
88 override_path = os.path.join(96 override_path = os.path.join(
89 init_dir, '{}.override'.format(service_name))97 init_dir, '{}.override'.format(service_name))
90 if os.path.exists(override_path):98 if os.path.exists(override_path):
91 os.unlink(override_path)99 os.unlink(override_path)
100 elif os.path.exists(sysv_file):
101 subprocess.check_call(["update-rc.d", service_name, "enable"])
102 else:
103 # XXX: Support SystemD too
104 raise ValueError(
105 "Unable to detect {0} as either Upstart {1} or SysV {2}".format(
106 service_name, upstart_file, sysv_file))
107
92 started = service_start(service_name)108 started = service_start(service_name)
93 return started109 return started
94110
95111
=== modified file 'charmhelpers/core/hugepage.py'
--- charmhelpers/core/hugepage.py 2015-08-19 13:49:05 +0000
+++ charmhelpers/core/hugepage.py 2015-09-25 14:42:33 +0000
@@ -25,11 +25,13 @@
25 fstab_mount,25 fstab_mount,
26 mkdir,26 mkdir,
27)27)
28from charmhelpers.core.strutils import bytes_from_string
29from subprocess import check_output
2830
2931
30def hugepage_support(user, group='hugetlb', nr_hugepages=256,32def hugepage_support(user, group='hugetlb', nr_hugepages=256,
31 max_map_count=65536, mnt_point='/run/hugepages/kvm',33 max_map_count=65536, mnt_point='/run/hugepages/kvm',
32 pagesize='2MB', mount=True):34 pagesize='2MB', mount=True, set_shmmax=False):
33 """Enable hugepages on system.35 """Enable hugepages on system.
3436
35 Args:37 Args:
@@ -49,6 +51,11 @@
49 'vm.max_map_count': max_map_count,51 'vm.max_map_count': max_map_count,
50 'vm.hugetlb_shm_group': gid,52 'vm.hugetlb_shm_group': gid,
51 }53 }
54 if set_shmmax:
55 shmmax_current = int(check_output(['sysctl', '-n', 'kernel.shmmax']))
56 shmmax_minsize = bytes_from_string(pagesize) * nr_hugepages
57 if shmmax_minsize > shmmax_current:
58 sysctl_settings['kernel.shmmax'] = shmmax_minsize
52 sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf')59 sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf')
53 mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False)60 mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False)
54 lfstab = fstab.Fstab()61 lfstab = fstab.Fstab()
5562
=== modified file 'charmhelpers/core/strutils.py'
--- charmhelpers/core/strutils.py 2015-04-16 19:55:16 +0000
+++ charmhelpers/core/strutils.py 2015-09-25 14:42:33 +0000
@@ -18,6 +18,7 @@
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1919
20import six20import six
21import re
2122
2223
23def bool_from_string(value):24def bool_from_string(value):
@@ -40,3 +41,32 @@
4041
41 msg = "Unable to interpret string value '%s' as boolean" % (value)42 msg = "Unable to interpret string value '%s' as boolean" % (value)
42 raise ValueError(msg)43 raise ValueError(msg)
44
45
46def bytes_from_string(value):
47 """Interpret human readable string value as bytes.
48
49 Returns int
50 """
51 BYTE_POWER = {
52 'K': 1,
53 'KB': 1,
54 'M': 2,
55 'MB': 2,
56 'G': 3,
57 'GB': 3,
58 'T': 4,
59 'TB': 4,
60 'P': 5,
61 'PB': 5,
62 }
63 if isinstance(value, six.string_types):
64 value = six.text_type(value)
65 else:
66 msg = "Unable to interpret non-string value '%s' as boolean" % (value)
67 raise ValueError(msg)
68 matches = re.match("([0-9]+)([a-zA-Z]+)", value)
69 if not matches:
70 msg = "Unable to interpret string value '%s' as bytes" % (value)
71 raise ValueError(msg)
72 return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])
4373
=== modified file 'tests/charmhelpers/contrib/amulet/deployment.py'
--- tests/charmhelpers/contrib/amulet/deployment.py 2015-03-11 11:45:09 +0000
+++ tests/charmhelpers/contrib/amulet/deployment.py 2015-09-25 14:42:33 +0000
@@ -51,7 +51,8 @@
51 if 'units' not in this_service:51 if 'units' not in this_service:
52 this_service['units'] = 152 this_service['units'] = 1
5353
54 self.d.add(this_service['name'], units=this_service['units'])54 self.d.add(this_service['name'], units=this_service['units'],
55 constraints=this_service.get('constraints'))
5556
56 for svc in other_services:57 for svc in other_services:
57 if 'location' in svc:58 if 'location' in svc:
@@ -64,7 +65,8 @@
64 if 'units' not in svc:65 if 'units' not in svc:
65 svc['units'] = 166 svc['units'] = 1
6667
67 self.d.add(svc['name'], charm=branch_location, units=svc['units'])68 self.d.add(svc['name'], charm=branch_location, units=svc['units'],
69 constraints=svc.get('constraints'))
6870
69 def _add_relations(self, relations):71 def _add_relations(self, relations):
70 """Add all of the relations for the services."""72 """Add all of the relations for the services."""
7173
=== modified file 'tests/charmhelpers/contrib/amulet/utils.py'
--- tests/charmhelpers/contrib/amulet/utils.py 2015-09-02 02:20:28 +0000
+++ tests/charmhelpers/contrib/amulet/utils.py 2015-09-25 14:42:33 +0000
@@ -19,9 +19,11 @@
19import logging19import logging
20import os20import os
21import re21import re
22import socket
22import subprocess23import subprocess
23import sys24import sys
24import time25import time
26import uuid
2527
26import amulet28import amulet
27import distro_info29import distro_info
@@ -324,7 +326,7 @@
324326
325 def service_restarted_since(self, sentry_unit, mtime, service,327 def service_restarted_since(self, sentry_unit, mtime, service,
326 pgrep_full=None, sleep_time=20,328 pgrep_full=None, sleep_time=20,
327 retry_count=2, retry_sleep_time=30):329 retry_count=30, retry_sleep_time=10):
328 """Check if service was been started after a given time.330 """Check if service was been started after a given time.
329331
330 Args:332 Args:
@@ -332,8 +334,9 @@
332 mtime (float): The epoch time to check against334 mtime (float): The epoch time to check against
333 service (string): service name to look for in process table335 service (string): service name to look for in process table
334 pgrep_full: [Deprecated] Use full command line search mode with pgrep336 pgrep_full: [Deprecated] Use full command line search mode with pgrep
335 sleep_time (int): Seconds to sleep before looking for process337 sleep_time (int): Initial sleep time (s) before looking for file
336 retry_count (int): If service is not found, how many times to retry338 retry_sleep_time (int): Time (s) to sleep between retries
339 retry_count (int): If file is not found, how many times to retry
337340
338 Returns:341 Returns:
339 bool: True if service found and its start time it newer than mtime,342 bool: True if service found and its start time it newer than mtime,
@@ -357,11 +360,12 @@
357 pgrep_full)360 pgrep_full)
358 self.log.debug('Attempt {} to get {} proc start time on {} '361 self.log.debug('Attempt {} to get {} proc start time on {} '
359 'OK'.format(tries, service, unit_name))362 'OK'.format(tries, service, unit_name))
360 except IOError:363 except IOError as e:
361 # NOTE(beisner) - race avoidance, proc may not exist yet.364 # NOTE(beisner) - race avoidance, proc may not exist yet.
362 # https://bugs.launchpad.net/charm-helpers/+bug/1474030365 # https://bugs.launchpad.net/charm-helpers/+bug/1474030
363 self.log.debug('Attempt {} to get {} proc start time on {} '366 self.log.debug('Attempt {} to get {} proc start time on {} '
364 'failed'.format(tries, service, unit_name))367 'failed\n{}'.format(tries, service,
368 unit_name, e))
365 time.sleep(retry_sleep_time)369 time.sleep(retry_sleep_time)
366 tries += 1370 tries += 1
367371
@@ -381,38 +385,62 @@
381 return False385 return False
382386
383 def config_updated_since(self, sentry_unit, filename, mtime,387 def config_updated_since(self, sentry_unit, filename, mtime,
384 sleep_time=20):388 sleep_time=20, retry_count=30,
389 retry_sleep_time=10):
385 """Check if file was modified after a given time.390 """Check if file was modified after a given time.
386391
387 Args:392 Args:
388 sentry_unit (sentry): The sentry unit to check the file mtime on393 sentry_unit (sentry): The sentry unit to check the file mtime on
389 filename (string): The file to check mtime of394 filename (string): The file to check mtime of
390 mtime (float): The epoch time to check against395 mtime (float): The epoch time to check against
391 sleep_time (int): Seconds to sleep before looking for process396 sleep_time (int): Initial sleep time (s) before looking for file
397 retry_sleep_time (int): Time (s) to sleep between retries
398 retry_count (int): If file is not found, how many times to retry
392399
393 Returns:400 Returns:
394 bool: True if file was modified more recently than mtime, False if401 bool: True if file was modified more recently than mtime, False if
395 file was modified before mtime,402 file was modified before mtime, or if file not found.
396 """403 """
397 self.log.debug('Checking that %s file updated since '
398 '%s' % (filename, mtime))
399 unit_name = sentry_unit.info['unit_name']404 unit_name = sentry_unit.info['unit_name']
405 self.log.debug('Checking that %s updated since %s on '
406 '%s' % (filename, mtime, unit_name))
400 time.sleep(sleep_time)407 time.sleep(sleep_time)
401 file_mtime = self._get_file_mtime(sentry_unit, filename)408 file_mtime = None
409 tries = 0
410 while tries <= retry_count and not file_mtime:
411 try:
412 file_mtime = self._get_file_mtime(sentry_unit, filename)
413 self.log.debug('Attempt {} to get {} file mtime on {} '
414 'OK'.format(tries, filename, unit_name))
415 except IOError as e:
416 # NOTE(beisner) - race avoidance, file may not exist yet.
417 # https://bugs.launchpad.net/charm-helpers/+bug/1474030
418 self.log.debug('Attempt {} to get {} file mtime on {} '
419 'failed\n{}'.format(tries, filename,
420 unit_name, e))
421 time.sleep(retry_sleep_time)
422 tries += 1
423
424 if not file_mtime:
425 self.log.warn('Could not determine file mtime, assuming '
426 'file does not exist')
427 return False
428
402 if file_mtime >= mtime:429 if file_mtime >= mtime:
403 self.log.debug('File mtime is newer than provided mtime '430 self.log.debug('File mtime is newer than provided mtime '
404 '(%s >= %s) on %s (OK)' % (file_mtime, mtime,431 '(%s >= %s) on %s (OK)' % (file_mtime,
405 unit_name))432 mtime, unit_name))
406 return True433 return True
407 else:434 else:
408 self.log.warn('File mtime %s is older than provided mtime %s'435 self.log.warn('File mtime is older than provided mtime'
409 % (file_mtime, mtime))436 '(%s < on %s) on %s' % (file_mtime,
437 mtime, unit_name))
410 return False438 return False
411439
412 def validate_service_config_changed(self, sentry_unit, mtime, service,440 def validate_service_config_changed(self, sentry_unit, mtime, service,
413 filename, pgrep_full=None,441 filename, pgrep_full=None,
414 sleep_time=20, retry_count=2,442 sleep_time=20, retry_count=30,
415 retry_sleep_time=30):443 retry_sleep_time=10):
416 """Check service and file were updated after mtime444 """Check service and file were updated after mtime
417445
418 Args:446 Args:
@@ -457,7 +485,9 @@
457 sentry_unit,485 sentry_unit,
458 filename,486 filename,
459 mtime,487 mtime,
460 sleep_time=0)488 sleep_time=sleep_time,
489 retry_count=retry_count,
490 retry_sleep_time=retry_sleep_time)
461491
462 return service_restart and config_update492 return service_restart and config_update
463493
@@ -476,7 +506,6 @@
476 """Return a list of all Ubuntu releases in order of release."""506 """Return a list of all Ubuntu releases in order of release."""
477 _d = distro_info.UbuntuDistroInfo()507 _d = distro_info.UbuntuDistroInfo()
478 _release_list = _d.all508 _release_list = _d.all
479 self.log.debug('Ubuntu release list: {}'.format(_release_list))
480 return _release_list509 return _release_list
481510
482 def file_to_url(self, file_rel_path):511 def file_to_url(self, file_rel_path):
@@ -616,6 +645,142 @@
616645
617 return None646 return None
618647
648 def validate_sectionless_conf(self, file_contents, expected):
649 """A crude conf parser. Useful to inspect configuration files which
650 do not have section headers (as would be necessary in order to use
651 the configparser). Such as openstack-dashboard or rabbitmq confs."""
652 for line in file_contents.split('\n'):
653 if '=' in line:
654 args = line.split('=')
655 if len(args) <= 1:
656 continue
657 key = args[0].strip()
658 value = args[1].strip()
659 if key in expected.keys():
660 if expected[key] != value:
661 msg = ('Config mismatch. Expected, actual: {}, '
662 '{}'.format(expected[key], value))
663 amulet.raise_status(amulet.FAIL, msg=msg)
664
665 def get_unit_hostnames(self, units):
666 """Return a dict of juju unit names to hostnames."""
667 host_names = {}
668 for unit in units:
669 host_names[unit.info['unit_name']] = \
670 str(unit.file_contents('/etc/hostname').strip())
671 self.log.debug('Unit host names: {}'.format(host_names))
672 return host_names
673
674 def run_cmd_unit(self, sentry_unit, cmd):
675 """Run a command on a unit, return the output and exit code."""
676 output, code = sentry_unit.run(cmd)
677 if code == 0:
678 self.log.debug('{} `{}` command returned {} '
679 '(OK)'.format(sentry_unit.info['unit_name'],
680 cmd, code))
681 else:
682 msg = ('{} `{}` command returned {} '
683 '{}'.format(sentry_unit.info['unit_name'],
684 cmd, code, output))
685 amulet.raise_status(amulet.FAIL, msg=msg)
686 return str(output), code
687
688 def file_exists_on_unit(self, sentry_unit, file_name):
689 """Check if a file exists on a unit."""
690 try:
691 sentry_unit.file_stat(file_name)
692 return True
693 except IOError:
694 return False
695 except Exception as e:
696 msg = 'Error checking file {}: {}'.format(file_name, e)
697 amulet.raise_status(amulet.FAIL, msg=msg)
698
699 def file_contents_safe(self, sentry_unit, file_name,
700 max_wait=60, fatal=False):
701 """Get file contents from a sentry unit. Wrap amulet file_contents
702 with retry logic to address races where a file checks as existing,
703 but no longer exists by the time file_contents is called.
704 Return None if file not found. Optionally raise if fatal is True."""
705 unit_name = sentry_unit.info['unit_name']
706 file_contents = False
707 tries = 0
708 while not file_contents and tries < (max_wait / 4):
709 try:
710 file_contents = sentry_unit.file_contents(file_name)
711 except IOError:
712 self.log.debug('Attempt {} to open file {} from {} '
713 'failed'.format(tries, file_name,
714 unit_name))
715 time.sleep(4)
716 tries += 1
717
718 if file_contents:
719 return file_contents
720 elif not fatal:
721 return None
722 elif fatal:
723 msg = 'Failed to get file contents from unit.'
724 amulet.raise_status(amulet.FAIL, msg)
725
726 def port_knock_tcp(self, host="localhost", port=22, timeout=15):
727 """Open a TCP socket to check for a listening sevice on a host.
728
729 :param host: host name or IP address, default to localhost
730 :param port: TCP port number, default to 22
731 :param timeout: Connect timeout, default to 15 seconds
732 :returns: True if successful, False if connect failed
733 """
734
735 # Resolve host name if possible
736 try:
737 connect_host = socket.gethostbyname(host)
738 host_human = "{} ({})".format(connect_host, host)
739 except socket.error as e:
740 self.log.warn('Unable to resolve address: '
741 '{} ({}) Trying anyway!'.format(host, e))
742 connect_host = host
743 host_human = connect_host
744
745 # Attempt socket connection
746 try:
747 knock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
748 knock.settimeout(timeout)
749 knock.connect((connect_host, port))
750 knock.close()
751 self.log.debug('Socket connect OK for host '
752 '{} on port {}.'.format(host_human, port))
753 return True
754 except socket.error as e:
755 self.log.debug('Socket connect FAIL for'
756 ' {} port {} ({})'.format(host_human, port, e))
757 return False
758
759 def port_knock_units(self, sentry_units, port=22,
760 timeout=15, expect_success=True):
761 """Open a TCP socket to check for a listening sevice on each
762 listed juju unit.
763
764 :param sentry_units: list of sentry unit pointers
765 :param port: TCP port number, default to 22
766 :param timeout: Connect timeout, default to 15 seconds
767 :expect_success: True by default, set False to invert logic
768 :returns: None if successful, Failure message otherwise
769 """
770 for unit in sentry_units:
771 host = unit.info['public-address']
772 connected = self.port_knock_tcp(host, port, timeout)
773 if not connected and expect_success:
774 return 'Socket connect failed.'
775 elif connected and not expect_success:
776 return 'Socket connected unexpectedly.'
777
778 def get_uuid_epoch_stamp(self):
779 """Returns a stamp string based on uuid4 and epoch time. Useful in
780 generating test messages which need to be unique-ish."""
781 return '[{}-{}]'.format(uuid.uuid4(), time.time())
782
783# amulet juju action helpers:
619 def run_action(self, unit_sentry, action,784 def run_action(self, unit_sentry, action,
620 _check_output=subprocess.check_output):785 _check_output=subprocess.check_output):
621 """Run the named action on a given unit sentry.786 """Run the named action on a given unit sentry.
@@ -642,3 +807,12 @@
642 output = _check_output(command, universal_newlines=True)807 output = _check_output(command, universal_newlines=True)
643 data = json.loads(output)808 data = json.loads(output)
644 return data.get(u"status") == "completed"809 return data.get(u"status") == "completed"
810
811 def status_get(self, unit):
812 """Return the current service status of this unit."""
813 raw_status, return_code = unit.run(
814 "status-get --format=json --include-data")
815 if return_code != 0:
816 return ("unknown", "")
817 status = json.loads(raw_status)
818 return (status["status"], status["message"])
645819
=== modified file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-08-19 13:53:50 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-09-25 14:42:33 +0000
@@ -44,20 +44,31 @@
44 Determine if the local branch being tested is derived from its44 Determine if the local branch being tested is derived from its
45 stable or next (dev) branch, and based on this, use the corresonding45 stable or next (dev) branch, and based on this, use the corresonding
46 stable or next branches for the other_services."""46 stable or next branches for the other_services."""
47
48 # Charms outside the lp:~openstack-charmers namespace
47 base_charms = ['mysql', 'mongodb', 'nrpe']49 base_charms = ['mysql', 'mongodb', 'nrpe']
4850
51 # Force these charms to current series even when using an older series.
52 # ie. Use trusty/nrpe even when series is precise, as the P charm
53 # does not possess the necessary external master config and hooks.
54 force_series_current = ['nrpe']
55
49 if self.series in ['precise', 'trusty']:56 if self.series in ['precise', 'trusty']:
50 base_series = self.series57 base_series = self.series
51 else:58 else:
52 base_series = self.current_next59 base_series = self.current_next
5360
54 if self.stable:61 for svc in other_services:
55 for svc in other_services:62 if svc['name'] in force_series_current:
63 base_series = self.current_next
64 # If a location has been explicitly set, use it
65 if svc.get('location'):
66 continue
67 if self.stable:
56 temp = 'lp:charms/{}/{}'68 temp = 'lp:charms/{}/{}'
57 svc['location'] = temp.format(base_series,69 svc['location'] = temp.format(base_series,
58 svc['name'])70 svc['name'])
59 else:71 else:
60 for svc in other_services:
61 if svc['name'] in base_charms:72 if svc['name'] in base_charms:
62 temp = 'lp:charms/{}/{}'73 temp = 'lp:charms/{}/{}'
63 svc['location'] = temp.format(base_series,74 svc['location'] = temp.format(base_series,
@@ -66,6 +77,7 @@
66 temp = 'lp:~openstack-charmers/charms/{}/{}/next'77 temp = 'lp:~openstack-charmers/charms/{}/{}/next'
67 svc['location'] = temp.format(self.current_next,78 svc['location'] = temp.format(self.current_next,
68 svc['name'])79 svc['name'])
80
69 return other_services81 return other_services
7082
71 def _add_services(self, this_service, other_services):83 def _add_services(self, this_service, other_services):
@@ -77,21 +89,23 @@
7789
78 services = other_services90 services = other_services
79 services.append(this_service)91 services.append(this_service)
92
93 # Charms which should use the source config option
80 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',94 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
81 'ceph-osd', 'ceph-radosgw']95 'ceph-osd', 'ceph-radosgw']
82 # Most OpenStack subordinate charms do not expose an origin option96
83 # as that is controlled by the principle.97 # Charms which can not use openstack-origin, ie. many subordinates
84 ignore = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']98 no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']
8599
86 if self.openstack:100 if self.openstack:
87 for svc in services:101 for svc in services:
88 if svc['name'] not in use_source + ignore:102 if svc['name'] not in use_source + no_origin:
89 config = {'openstack-origin': self.openstack}103 config = {'openstack-origin': self.openstack}
90 self.d.configure(svc['name'], config)104 self.d.configure(svc['name'], config)
91105
92 if self.source:106 if self.source:
93 for svc in services:107 for svc in services:
94 if svc['name'] in use_source and svc['name'] not in ignore:108 if svc['name'] in use_source and svc['name'] not in no_origin:
95 config = {'source': self.source}109 config = {'source': self.source}
96 self.d.configure(svc['name'], config)110 self.d.configure(svc['name'], config)
97111
98112
=== modified file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
--- tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-07-01 04:03:20 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-09-25 14:42:33 +0000
@@ -27,6 +27,7 @@
27import heatclient.v1.client as heat_client27import heatclient.v1.client as heat_client
28import keystoneclient.v2_0 as keystone_client28import keystoneclient.v2_0 as keystone_client
29import novaclient.v1_1.client as nova_client29import novaclient.v1_1.client as nova_client
30import pika
30import swiftclient31import swiftclient
3132
32from charmhelpers.contrib.amulet.utils import (33from charmhelpers.contrib.amulet.utils import (
@@ -602,3 +603,361 @@
602 self.log.debug('Ceph {} samples (OK): '603 self.log.debug('Ceph {} samples (OK): '
603 '{}'.format(sample_type, samples))604 '{}'.format(sample_type, samples))
604 return None605 return None
606
607# rabbitmq/amqp specific helpers:
608 def add_rmq_test_user(self, sentry_units,
609 username="testuser1", password="changeme"):
610 """Add a test user via the first rmq juju unit, check connection as
611 the new user against all sentry units.
612
613 :param sentry_units: list of sentry unit pointers
614 :param username: amqp user name, default to testuser1
615 :param password: amqp user password
616 :returns: None if successful. Raise on error.
617 """
618 self.log.debug('Adding rmq user ({})...'.format(username))
619
620 # Check that user does not already exist
621 cmd_user_list = 'rabbitmqctl list_users'
622 output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
623 if username in output:
624 self.log.warning('User ({}) already exists, returning '
625 'gracefully.'.format(username))
626 return
627
628 perms = '".*" ".*" ".*"'
629 cmds = ['rabbitmqctl add_user {} {}'.format(username, password),
630 'rabbitmqctl set_permissions {} {}'.format(username, perms)]
631
632 # Add user via first unit
633 for cmd in cmds:
634 output, _ = self.run_cmd_unit(sentry_units[0], cmd)
635
636 # Check connection against the other sentry_units
637 self.log.debug('Checking user connect against units...')
638 for sentry_unit in sentry_units:
639 connection = self.connect_amqp_by_unit(sentry_unit, ssl=False,
640 username=username,
641 password=password)
642 connection.close()
643
644 def delete_rmq_test_user(self, sentry_units, username="testuser1"):
645 """Delete a rabbitmq user via the first rmq juju unit.
646
647 :param sentry_units: list of sentry unit pointers
648 :param username: amqp user name, default to testuser1
649 :param password: amqp user password
650 :returns: None if successful or no such user.
651 """
652 self.log.debug('Deleting rmq user ({})...'.format(username))
653
654 # Check that the user exists
655 cmd_user_list = 'rabbitmqctl list_users'
656 output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
657
658 if username not in output:
659 self.log.warning('User ({}) does not exist, returning '
660 'gracefully.'.format(username))
661 return
662
663 # Delete the user
664 cmd_user_del = 'rabbitmqctl delete_user {}'.format(username)
665 output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_del)
666
667 def get_rmq_cluster_status(self, sentry_unit):
668 """Execute rabbitmq cluster status command on a unit and return
669 the full output.
670
671 :param unit: sentry unit
672 :returns: String containing console output of cluster status command
673 """
674 cmd = 'rabbitmqctl cluster_status'
675 output, _ = self.run_cmd_unit(sentry_unit, cmd)
676 self.log.debug('{} cluster_status:\n{}'.format(
677 sentry_unit.info['unit_name'], output))
678 return str(output)
679
680 def get_rmq_cluster_running_nodes(self, sentry_unit):
681 """Parse rabbitmqctl cluster_status output string, return list of
682 running rabbitmq cluster nodes.
683
684 :param unit: sentry unit
685 :returns: List containing node names of running nodes
686 """
687 # NOTE(beisner): rabbitmqctl cluster_status output is not
688 # json-parsable, do string chop foo, then json.loads that.
689 str_stat = self.get_rmq_cluster_status(sentry_unit)
690 if 'running_nodes' in str_stat:
691 pos_start = str_stat.find("{running_nodes,") + 15
692 pos_end = str_stat.find("]},", pos_start) + 1
693 str_run_nodes = str_stat[pos_start:pos_end].replace("'", '"')
694 run_nodes = json.loads(str_run_nodes)
695 return run_nodes
696 else:
697 return []
698
699 def validate_rmq_cluster_running_nodes(self, sentry_units):
700 """Check that all rmq unit hostnames are represented in the
701 cluster_status output of all units.
702
703 :param host_names: dict of juju unit names to host names
704 :param units: list of sentry unit pointers (all rmq units)
705 :returns: None if successful, otherwise return error message
706 """
707 host_names = self.get_unit_hostnames(sentry_units)
708 errors = []
709
710 # Query every unit for cluster_status running nodes
711 for query_unit in sentry_units:
712 query_unit_name = query_unit.info['unit_name']
713 running_nodes = self.get_rmq_cluster_running_nodes(query_unit)
714
715 # Confirm that every unit is represented in the queried unit's
716 # cluster_status running nodes output.
717 for validate_unit in sentry_units:
718 val_host_name = host_names[validate_unit.info['unit_name']]
719 val_node_name = 'rabbit@{}'.format(val_host_name)
720
721 if val_node_name not in running_nodes:
722 errors.append('Cluster member check failed on {}: {} not '
723 'in {}\n'.format(query_unit_name,
724 val_node_name,
725 running_nodes))
726 if errors:
727 return ''.join(errors)
728
729 def rmq_ssl_is_enabled_on_unit(self, sentry_unit, port=None):
730 """Check a single juju rmq unit for ssl and port in the config file."""
731 host = sentry_unit.info['public-address']
732 unit_name = sentry_unit.info['unit_name']
733
734 conf_file = '/etc/rabbitmq/rabbitmq.config'
735 conf_contents = str(self.file_contents_safe(sentry_unit,
736 conf_file, max_wait=16))
737 # Checks
738 conf_ssl = 'ssl' in conf_contents
739 conf_port = str(port) in conf_contents
740
741 # Port explicitly checked in config
742 if port and conf_port and conf_ssl:
743 self.log.debug('SSL is enabled @{}:{} '
744 '({})'.format(host, port, unit_name))
745 return True
746 elif port and not conf_port and conf_ssl:
747 self.log.debug('SSL is enabled @{} but not on port {} '
748 '({})'.format(host, port, unit_name))
749 return False
750 # Port not checked (useful when checking that ssl is disabled)
751 elif not port and conf_ssl:
752 self.log.debug('SSL is enabled @{}:{} '
753 '({})'.format(host, port, unit_name))
754 return True
755 elif not port and not conf_ssl:
756 self.log.debug('SSL not enabled @{}:{} '
757 '({})'.format(host, port, unit_name))
758 return False
759 else:
760 msg = ('Unknown condition when checking SSL status @{}:{} '
761 '({})'.format(host, port, unit_name))
762 amulet.raise_status(amulet.FAIL, msg)
763
764 def validate_rmq_ssl_enabled_units(self, sentry_units, port=None):
765 """Check that ssl is enabled on rmq juju sentry units.
766
767 :param sentry_units: list of all rmq sentry units
768 :param port: optional ssl port override to validate
769 :returns: None if successful, otherwise return error message
770 """
771 for sentry_unit in sentry_units:
772 if not self.rmq_ssl_is_enabled_on_unit(sentry_unit, port=port):
773 return ('Unexpected condition: ssl is disabled on unit '
774 '({})'.format(sentry_unit.info['unit_name']))
775 return None
776
777 def validate_rmq_ssl_disabled_units(self, sentry_units):
778 """Check that ssl is enabled on listed rmq juju sentry units.
779
780 :param sentry_units: list of all rmq sentry units
781 :returns: True if successful. Raise on error.
782 """
783 for sentry_unit in sentry_units:
784 if self.rmq_ssl_is_enabled_on_unit(sentry_unit):
785 return ('Unexpected condition: ssl is enabled on unit '
786 '({})'.format(sentry_unit.info['unit_name']))
787 return None
788
789 def configure_rmq_ssl_on(self, sentry_units, deployment,
790 port=None, max_wait=60):
791 """Turn ssl charm config option on, with optional non-default
792 ssl port specification. Confirm that it is enabled on every
793 unit.
794
795 :param sentry_units: list of sentry units
796 :param deployment: amulet deployment object pointer
797 :param port: amqp port, use defaults if None
798 :param max_wait: maximum time to wait in seconds to confirm
799 :returns: None if successful. Raise on error.
800 """
801 self.log.debug('Setting ssl charm config option: on')
802
803 # Enable RMQ SSL
804 config = {'ssl': 'on'}
805 if port:
806 config['ssl_port'] = port
807
808 deployment.configure('rabbitmq-server', config)
809
810 # Confirm
811 tries = 0
812 ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
813 while ret and tries < (max_wait / 4):
814 time.sleep(4)
815 self.log.debug('Attempt {}: {}'.format(tries, ret))
816 ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
817 tries += 1
818
819 if ret:
820 amulet.raise_status(amulet.FAIL, ret)
821
822 def configure_rmq_ssl_off(self, sentry_units, deployment, max_wait=60):
823 """Turn ssl charm config option off, confirm that it is disabled
824 on every unit.
825
826 :param sentry_units: list of sentry units
827 :param deployment: amulet deployment object pointer
828 :param max_wait: maximum time to wait in seconds to confirm
829 :returns: None if successful. Raise on error.
830 """
831 self.log.debug('Setting ssl charm config option: off')
832
833 # Disable RMQ SSL
834 config = {'ssl': 'off'}
835 deployment.configure('rabbitmq-server', config)
836
837 # Confirm
838 tries = 0
839 ret = self.validate_rmq_ssl_disabled_units(sentry_units)
840 while ret and tries < (max_wait / 4):
841 time.sleep(4)
842 self.log.debug('Attempt {}: {}'.format(tries, ret))
843 ret = self.validate_rmq_ssl_disabled_units(sentry_units)
844 tries += 1
845
846 if ret:
847 amulet.raise_status(amulet.FAIL, ret)
848
849 def connect_amqp_by_unit(self, sentry_unit, ssl=False,
850 port=None, fatal=True,
851 username="testuser1", password="changeme"):
852 """Establish and return a pika amqp connection to the rabbitmq service
853 running on a rmq juju unit.
854
855 :param sentry_unit: sentry unit pointer
856 :param ssl: boolean, default to False
857 :param port: amqp port, use defaults if None
858 :param fatal: boolean, default to True (raises on connect error)
859 :param username: amqp user name, default to testuser1
860 :param password: amqp user password
861 :returns: pika amqp connection pointer or None if failed and non-fatal
862 """
863 host = sentry_unit.info['public-address']
864 unit_name = sentry_unit.info['unit_name']
865
866 # Default port logic if port is not specified
867 if ssl and not port:
868 port = 5671
869 elif not ssl and not port:
870 port = 5672
871
872 self.log.debug('Connecting to amqp on {}:{} ({}) as '
873 '{}...'.format(host, port, unit_name, username))
874
875 try:
876 credentials = pika.PlainCredentials(username, password)
877 parameters = pika.ConnectionParameters(host=host, port=port,
878 credentials=credentials,
879 ssl=ssl,
880 connection_attempts=3,
881 retry_delay=5,
882 socket_timeout=1)
883 connection = pika.BlockingConnection(parameters)
884 assert connection.server_properties['product'] == 'RabbitMQ'
885 self.log.debug('Connect OK')
886 return connection
887 except Exception as e:
888 msg = ('amqp connection failed to {}:{} as '
889 '{} ({})'.format(host, port, username, str(e)))
890 if fatal:
891 amulet.raise_status(amulet.FAIL, msg)
892 else:
893 self.log.warn(msg)
894 return None
895
896 def publish_amqp_message_by_unit(self, sentry_unit, message,
897 queue="test", ssl=False,
898 username="testuser1",
899 password="changeme",
900 port=None):
901 """Publish an amqp message to a rmq juju unit.
902
903 :param sentry_unit: sentry unit pointer
904 :param message: amqp message string
905 :param queue: message queue, default to test
906 :param username: amqp user name, default to testuser1
907 :param password: amqp user password
908 :param ssl: boolean, default to False
909 :param port: amqp port, use defaults if None
910 :returns: None. Raises exception if publish failed.
911 """
912 self.log.debug('Publishing message to {} queue:\n{}'.format(queue,
913 message))
914 connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
915 port=port,
916 username=username,
917 password=password)
918
919 # NOTE(beisner): extra debug here re: pika hang potential:
920 # https://github.com/pika/pika/issues/297
921 # https://groups.google.com/forum/#!topic/rabbitmq-users/Ja0iyfF0Szw
922 self.log.debug('Defining channel...')
923 channel = connection.channel()
924 self.log.debug('Declaring queue...')
925 channel.queue_declare(queue=queue, auto_delete=False, durable=True)
926 self.log.debug('Publishing message...')
927 channel.basic_publish(exchange='', routing_key=queue, body=message)
928 self.log.debug('Closing channel...')
929 channel.close()
930 self.log.debug('Closing connection...')
931 connection.close()
932
933 def get_amqp_message_by_unit(self, sentry_unit, queue="test",
934 username="testuser1",
935 password="changeme",
936 ssl=False, port=None):
937 """Get an amqp message from a rmq juju unit.
938
939 :param sentry_unit: sentry unit pointer
940 :param queue: message queue, default to test
941 :param username: amqp user name, default to testuser1
942 :param password: amqp user password
943 :param ssl: boolean, default to False
944 :param port: amqp port, use defaults if None
945 :returns: amqp message body as string. Raise if get fails.
946 """
947 connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
948 port=port,
949 username=username,
950 password=password)
951 channel = connection.channel()
952 method_frame, _, body = channel.basic_get(queue)
953
954 if method_frame:
955 self.log.debug('Retreived message from {} queue:\n{}'.format(queue,
956 body))
957 channel.basic_ack(method_frame.delivery_tag)
958 channel.close()
959 connection.close()
960 return body
961 else:
962 msg = 'No message retrieved.'
963 amulet.raise_status(amulet.FAIL, msg)
605964
=== modified file 'unit_tests/test_keystone_hooks.py'
--- unit_tests/test_keystone_hooks.py 2015-08-14 16:10:45 +0000
+++ unit_tests/test_keystone_hooks.py 2015-09-25 14:42:33 +0000
@@ -142,7 +142,8 @@
142142
143 cfg_dict = {'prefer-ipv6': False,143 cfg_dict = {'prefer-ipv6': False,
144 'database': 'keystone',144 'database': 'keystone',
145 'database-user': 'keystone'}145 'database-user': 'keystone',
146 'vip': None}
146147
147 class mock_cls_config():148 class mock_cls_config():
148 def __call__(self, key):149 def __call__(self, key):

Subscribers

People subscribed via source and target branches