Merge lp:~xianghui/charms/trusty/nova-cloud-controller/live_migration into lp:~openstack-charmers-archive/charms/trusty/nova-cloud-controller/next

Proposed by Xiang Hui
Status: Merged
Merged at revision: 189
Proposed branch: lp:~xianghui/charms/trusty/nova-cloud-controller/live_migration
Merge into: lp:~openstack-charmers-archive/charms/trusty/nova-cloud-controller/next
Diff against target: 2135 lines (+1496/-110)
15 files modified
hooks/charmhelpers/contrib/network/ip.py (+5/-3)
hooks/charmhelpers/contrib/openstack/amulet/deployment.py (+13/-6)
hooks/charmhelpers/contrib/openstack/amulet/utils.py (+359/-0)
hooks/charmhelpers/contrib/openstack/context.py (+60/-16)
hooks/charmhelpers/contrib/openstack/templating.py (+29/-2)
hooks/charmhelpers/contrib/openstack/utils.py (+221/-1)
hooks/charmhelpers/contrib/storage/linux/ceph.py (+226/-13)
hooks/charmhelpers/core/host.py (+3/-4)
hooks/charmhelpers/core/kernel.py (+68/-0)
hooks/charmhelpers/core/strutils.py (+30/-0)
hooks/nova_cc_utils.py (+17/-5)
tests/charmhelpers/contrib/amulet/deployment.py (+4/-2)
tests/charmhelpers/contrib/amulet/utils.py (+96/-52)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+359/-0)
unit_tests/test_nova_cc_utils.py (+6/-6)
To merge this branch: bzr merge lp:~xianghui/charms/trusty/nova-cloud-controller/live_migration
Reviewer Review Type Date Requested Status
Billy Olsen Approve
Review via email: mp+267132@code.launchpad.net

Description of the change

Fix live-migration failed when re-add nova-compute unit.

To post a comment you must log in.
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #7609 nova-cloud-controller-next for xianghui mp267132
    LINT OK: passed

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

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

charm_unit_test #7047 nova-cloud-controller-next for xianghui mp267132
    UNIT OK: passed

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

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

charm_amulet_test #5646 nova-cloud-controller-next for xianghui mp267132
    AMULET FAIL: amulet-test failed

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

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

183. By Xiang Hui

Merge lp:charm-helpers.

184. By Xiang Hui

Fix conflicts.

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

charm_lint_check #10420 nova-cloud-controller-next for xianghui mp267132
    LINT FAIL: lint-test failed

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

Full lint test output: http://paste.ubuntu.com/12519228/
Build: http://10.245.162.77:8080/job/charm_lint_check/10420/

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

charm_unit_test #9617 nova-cloud-controller-next for xianghui mp267132
    UNIT OK: passed

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

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

charm_amulet_test #6564 nova-cloud-controller-next for xianghui mp267132
    AMULET FAIL: amulet-test failed

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

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

185. By Xiang Hui

Fix lint error

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

charm_lint_check #10428 nova-cloud-controller-next for xianghui mp267132
    LINT OK: passed

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

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

charm_unit_test #9619 nova-cloud-controller-next for xianghui mp267132
    UNIT OK: passed

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

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

charm_amulet_test #6571 nova-cloud-controller-next for xianghui mp267132
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/6571/

Revision history for this message
Billy Olsen (billy-olsen) wrote :

LGTM, thanks Xiang Hui!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'hooks/charmhelpers/contrib/network/ip.py'
--- hooks/charmhelpers/contrib/network/ip.py 2015-09-03 09:39:34 +0000
+++ hooks/charmhelpers/contrib/network/ip.py 2015-09-22 06:12:36 +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 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-08-18 17:34:35 +0000
+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-09-22 06:12:36 +0000
@@ -51,13 +51,17 @@
51 else:51 else:
52 base_series = self.current_next52 base_series = self.current_next
5353
54 if self.stable:54 for svc in other_services:
55 for svc in other_services:55 if svc['name'] in force_series_current:
56 base_series = self.current_next
57 # If a location has been explicitly set, use it
58 if svc.get('location'):
59 continue
60 if self.stable:
56 temp = 'lp:charms/{}/{}'61 temp = 'lp:charms/{}/{}'
57 svc['location'] = temp.format(base_series,62 svc['location'] = temp.format(base_series,
58 svc['name'])63 svc['name'])
59 else:64 else:
60 for svc in other_services:
61 if svc['name'] in base_charms:65 if svc['name'] in base_charms:
62 temp = 'lp:charms/{}/{}'66 temp = 'lp:charms/{}/{}'
63 svc['location'] = temp.format(base_series,67 svc['location'] = temp.format(base_series,
@@ -66,6 +70,7 @@
66 temp = 'lp:~openstack-charmers/charms/{}/{}/next'70 temp = 'lp:~openstack-charmers/charms/{}/{}/next'
67 svc['location'] = temp.format(self.current_next,71 svc['location'] = temp.format(self.current_next,
68 svc['name'])72 svc['name'])
73
69 return other_services74 return other_services
7075
71 def _add_services(self, this_service, other_services):76 def _add_services(self, this_service, other_services):
@@ -77,6 +82,8 @@
7782
78 services = other_services83 services = other_services
79 services.append(this_service)84 services.append(this_service)
85
86 # Charms which should use the source config option
80 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',87 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
81 'ceph-osd', 'ceph-radosgw']88 'ceph-osd', 'ceph-radosgw']
82 # Most OpenStack subordinate charms do not expose an origin option89 # Most OpenStack subordinate charms do not expose an origin option
@@ -85,13 +92,13 @@
8592
86 if self.openstack:93 if self.openstack:
87 for svc in services:94 for svc in services:
88 if svc['name'] not in use_source + ignore:95 if svc['name'] not in use_source + no_origin:
89 config = {'openstack-origin': self.openstack}96 config = {'openstack-origin': self.openstack}
90 self.d.configure(svc['name'], config)97 self.d.configure(svc['name'], config)
9198
92 if self.source:99 if self.source:
93 for svc in services:100 for svc in services:
94 if svc['name'] in use_source and svc['name'] not in ignore:101 if svc['name'] in use_source and svc['name'] not in no_origin:
95 config = {'source': self.source}102 config = {'source': self.source}
96 self.d.configure(svc['name'], config)103 self.d.configure(svc['name'], config)
97104
98105
=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/utils.py'
--- hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-07-16 20:18:38 +0000
+++ hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-09-22 06:12:36 +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 'hooks/charmhelpers/contrib/openstack/context.py'
--- hooks/charmhelpers/contrib/openstack/context.py 2015-09-03 09:39:34 +0000
+++ hooks/charmhelpers/contrib/openstack/context.py 2015-09-22 06:12:36 +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 'hooks/charmhelpers/contrib/openstack/templating.py'
--- hooks/charmhelpers/contrib/openstack/templating.py 2015-07-29 10:46:43 +0000
+++ hooks/charmhelpers/contrib/openstack/templating.py 2015-09-22 06:12:36 +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,
@@ -112,7 +112,7 @@
112112
113 def complete_contexts(self):113 def complete_contexts(self):
114 '''114 '''
115 Return a list of interfaces that have atisfied contexts.115 Return a list of interfaces that have satisfied contexts.
116 '''116 '''
117 if self._complete_contexts:117 if self._complete_contexts:
118 return self._complete_contexts118 return self._complete_contexts
@@ -293,3 +293,30 @@
293 [interfaces.extend(i.complete_contexts())293 [interfaces.extend(i.complete_contexts())
294 for i in six.itervalues(self.templates)]294 for i in six.itervalues(self.templates)]
295 return interfaces295 return interfaces
296
297 def get_incomplete_context_data(self, interfaces):
298 '''
299 Return dictionary of relation status of interfaces and any missing
300 required context data. Example:
301 {'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
302 'zeromq-configuration': {'related': False}}
303 '''
304 incomplete_context_data = {}
305
306 for i in six.itervalues(self.templates):
307 for context in i.contexts:
308 for interface in interfaces:
309 related = False
310 if interface in context.interfaces:
311 related = context.get_related()
312 missing_data = context.missing_data
313 if missing_data:
314 incomplete_context_data[interface] = {'missing_data': missing_data}
315 if related:
316 if incomplete_context_data.get(interface):
317 incomplete_context_data[interface].update({'related': True})
318 else:
319 incomplete_context_data[interface] = {'related': True}
320 else:
321 incomplete_context_data[interface] = {'related': False}
322 return incomplete_context_data
296323
=== modified file 'hooks/charmhelpers/contrib/openstack/utils.py'
--- hooks/charmhelpers/contrib/openstack/utils.py 2015-09-03 09:39:34 +0000
+++ hooks/charmhelpers/contrib/openstack/utils.py 2015-09-22 06:12:36 +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 (
@@ -114,6 +119,7 @@
114 ('2.2.1', 'kilo'),119 ('2.2.1', 'kilo'),
115 ('2.2.2', 'kilo'),120 ('2.2.2', 'kilo'),
116 ('2.3.0', 'liberty'),121 ('2.3.0', 'liberty'),
122 ('2.4.0', 'liberty'),
117])123])
118124
119# >= Liberty version->codename mapping125# >= Liberty version->codename mapping
@@ -745,3 +751,217 @@
745 return projects[key]751 return projects[key]
746752
747 return None753 return None
754
755
756def os_workload_status(configs, required_interfaces, charm_func=None):
757 """
758 Decorator to set workload status based on complete contexts
759 """
760 def wrap(f):
761 @wraps(f)
762 def wrapped_f(*args, **kwargs):
763 # Run the original function first
764 f(*args, **kwargs)
765 # Set workload status now that contexts have been
766 # acted on
767 set_os_workload_status(configs, required_interfaces, charm_func)
768 return wrapped_f
769 return wrap
770
771
772def set_os_workload_status(configs, required_interfaces, charm_func=None):
773 """
774 Set workload status based on complete contexts.
775 status-set missing or incomplete contexts
776 and juju-log details of missing required data.
777 charm_func is a charm specific function to run checking
778 for charm specific requirements such as a VIP setting.
779 """
780 incomplete_rel_data = incomplete_relation_data(configs, required_interfaces)
781 state = 'active'
782 missing_relations = []
783 incomplete_relations = []
784 message = None
785 charm_state = None
786 charm_message = None
787
788 for generic_interface in incomplete_rel_data.keys():
789 related_interface = None
790 missing_data = {}
791 # Related or not?
792 for interface in incomplete_rel_data[generic_interface]:
793 if incomplete_rel_data[generic_interface][interface].get('related'):
794 related_interface = interface
795 missing_data = incomplete_rel_data[generic_interface][interface].get('missing_data')
796 # No relation ID for the generic_interface
797 if not related_interface:
798 juju_log("{} relation is missing and must be related for "
799 "functionality. ".format(generic_interface), 'WARN')
800 state = 'blocked'
801 if generic_interface not in missing_relations:
802 missing_relations.append(generic_interface)
803 else:
804 # Relation ID exists but no related unit
805 if not missing_data:
806 # Edge case relation ID exists but departing
807 if ('departed' in hook_name() or 'broken' in hook_name()) \
808 and related_interface in hook_name():
809 state = 'blocked'
810 if generic_interface not in missing_relations:
811 missing_relations.append(generic_interface)
812 juju_log("{} relation's interface, {}, "
813 "relationship is departed or broken "
814 "and is required for functionality."
815 "".format(generic_interface, related_interface), "WARN")
816 # Normal case relation ID exists but no related unit
817 # (joining)
818 else:
819 juju_log("{} relations's interface, {}, is related but has "
820 "no units in the relation."
821 "".format(generic_interface, related_interface), "INFO")
822 # Related unit exists and data missing on the relation
823 else:
824 juju_log("{} relation's interface, {}, is related awaiting "
825 "the following data from the relationship: {}. "
826 "".format(generic_interface, related_interface,
827 ", ".join(missing_data)), "INFO")
828 if state != 'blocked':
829 state = 'waiting'
830 if generic_interface not in incomplete_relations \
831 and generic_interface not in missing_relations:
832 incomplete_relations.append(generic_interface)
833
834 if missing_relations:
835 message = "Missing relations: {}".format(", ".join(missing_relations))
836 if incomplete_relations:
837 message += "; incomplete relations: {}" \
838 "".format(", ".join(incomplete_relations))
839 state = 'blocked'
840 elif incomplete_relations:
841 message = "Incomplete relations: {}" \
842 "".format(", ".join(incomplete_relations))
843 state = 'waiting'
844
845 # Run charm specific checks
846 if charm_func:
847 charm_state, charm_message = charm_func(configs)
848 if charm_state != 'active' and charm_state != 'unknown':
849 state = workload_state_compare(state, charm_state)
850 if message:
851 message = "{} {}".format(message, charm_message)
852 else:
853 message = charm_message
854
855 # Set to active if all requirements have been met
856 if state == 'active':
857 message = "Unit is ready"
858 juju_log(message, "INFO")
859
860 status_set(state, message)
861
862
863def workload_state_compare(current_workload_state, workload_state):
864 """ Return highest priority of two states"""
865 hierarchy = {'unknown': -1,
866 'active': 0,
867 'maintenance': 1,
868 'waiting': 2,
869 'blocked': 3,
870 }
871
872 if hierarchy.get(workload_state) is None:
873 workload_state = 'unknown'
874 if hierarchy.get(current_workload_state) is None:
875 current_workload_state = 'unknown'
876
877 # Set workload_state based on hierarchy of statuses
878 if hierarchy.get(current_workload_state) > hierarchy.get(workload_state):
879 return current_workload_state
880 else:
881 return workload_state
882
883
884def incomplete_relation_data(configs, required_interfaces):
885 """
886 Check complete contexts against required_interfaces
887 Return dictionary of incomplete relation data.
888
889 configs is an OSConfigRenderer object with configs registered
890
891 required_interfaces is a dictionary of required general interfaces
892 with dictionary values of possible specific interfaces.
893 Example:
894 required_interfaces = {'database': ['shared-db', 'pgsql-db']}
895
896 The interface is said to be satisfied if anyone of the interfaces in the
897 list has a complete context.
898
899 Return dictionary of incomplete or missing required contexts with relation
900 status of interfaces and any missing data points. Example:
901 {'message':
902 {'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
903 'zeromq-configuration': {'related': False}},
904 'identity':
905 {'identity-service': {'related': False}},
906 'database':
907 {'pgsql-db': {'related': False},
908 'shared-db': {'related': True}}}
909 """
910 complete_ctxts = configs.complete_contexts()
911 incomplete_relations = []
912 for svc_type in required_interfaces.keys():
913 # Avoid duplicates
914 found_ctxt = False
915 for interface in required_interfaces[svc_type]:
916 if interface in complete_ctxts:
917 found_ctxt = True
918 if not found_ctxt:
919 incomplete_relations.append(svc_type)
920 incomplete_context_data = {}
921 for i in incomplete_relations:
922 incomplete_context_data[i] = configs.get_incomplete_context_data(required_interfaces[i])
923 return incomplete_context_data
924
925
926def do_action_openstack_upgrade(package, upgrade_callback, configs):
927 """Perform action-managed OpenStack upgrade.
928
929 Upgrades packages to the configured openstack-origin version and sets
930 the corresponding action status as a result.
931
932 If the charm was installed from source we cannot upgrade it.
933 For backwards compatibility a config flag (action-managed-upgrade) must
934 be set for this code to run, otherwise a full service level upgrade will
935 fire on config-changed.
936
937 @param package: package name for determining if upgrade available
938 @param upgrade_callback: function callback to charm's upgrade function
939 @param configs: templating object derived from OSConfigRenderer class
940
941 @return: True if upgrade successful; False if upgrade failed or skipped
942 """
943 ret = False
944
945 if git_install_requested():
946 action_set({'outcome': 'installed from source, skipped upgrade.'})
947 else:
948 if openstack_upgrade_available(package):
949 if config('action-managed-upgrade'):
950 juju_log('Upgrading OpenStack release')
951
952 try:
953 upgrade_callback(configs=configs)
954 action_set({'outcome': 'success, upgrade completed.'})
955 ret = True
956 except:
957 action_set({'outcome': 'upgrade failed, see traceback.'})
958 action_set({'traceback': traceback.format_exc()})
959 action_fail('do_openstack_upgrade resulted in an '
960 'unexpected error')
961 else:
962 action_set({'outcome': 'action-managed-upgrade config is '
963 'False, skipped upgrade.'})
964 else:
965 action_set({'outcome': 'no upgrade available.'})
966
967 return ret
748968
=== modified file 'hooks/charmhelpers/contrib/storage/linux/ceph.py'
--- hooks/charmhelpers/contrib/storage/linux/ceph.py 2015-07-16 20:18:38 +0000
+++ hooks/charmhelpers/contrib/storage/linux/ceph.py 2015-09-22 06:12:36 +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 'hooks/charmhelpers/core/host.py'
--- hooks/charmhelpers/core/host.py 2015-08-19 13:48:32 +0000
+++ hooks/charmhelpers/core/host.py 2015-09-22 06:12:36 +0000
@@ -63,12 +63,10 @@
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 # XXX: Support systemd too
74 override_path = os.path.join(72 override_path = os.path.join(
@@ -78,7 +76,8 @@
78 return stopped76 return stopped
7977
8078
81def service_resume(service_name, init_dir=None):79def service_resume(service_name, init_dir="/etc/init",
80 initd_dir="/etc/init.d"):
82 """Resume a system service.81 """Resume a system service.
8382
84 Reenable starting again at boot. Start the service"""83 Reenable starting again at boot. Start the service"""
8584
=== added file 'hooks/charmhelpers/core/kernel.py'
--- hooks/charmhelpers/core/kernel.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/kernel.py 2015-09-22 06:12:36 +0000
@@ -0,0 +1,68 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Copyright 2014-2015 Canonical Limited.
5#
6# This file is part of charm-helpers.
7#
8# charm-helpers is free software: you can redistribute it and/or modify
9# it under the terms of the GNU Lesser General Public License version 3 as
10# published by the Free Software Foundation.
11#
12# charm-helpers is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
19
20__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
21
22from charmhelpers.core.hookenv import (
23 log,
24 INFO
25)
26
27from subprocess import check_call, check_output
28import re
29
30
31def modprobe(module, persist=True):
32 """Load a kernel module and configure for auto-load on reboot."""
33 cmd = ['modprobe', module]
34
35 log('Loading kernel module %s' % module, level=INFO)
36
37 check_call(cmd)
38 if persist:
39 with open('/etc/modules', 'r+') as modules:
40 if module not in modules.read():
41 modules.write(module)
42
43
44def rmmod(module, force=False):
45 """Remove a module from the linux kernel"""
46 cmd = ['rmmod']
47 if force:
48 cmd.append('-f')
49 cmd.append(module)
50 log('Removing kernel module %s' % module, level=INFO)
51 return check_call(cmd)
52
53
54def lsmod():
55 """Shows what kernel modules are currently loaded"""
56 return check_output(['lsmod'],
57 universal_newlines=True)
58
59
60def is_module_loaded(module):
61 """Checks if a kernel module is already loaded"""
62 matches = re.findall('^%s[ ]+' % module, lsmod(), re.M)
63 return len(matches) > 0
64
65
66def update_initramfs(version='all'):
67 """Updates an initramfs image"""
68 return check_call(["update-initramfs", "-k", version, "-u"])
069
=== modified file 'hooks/charmhelpers/core/strutils.py'
--- hooks/charmhelpers/core/strutils.py 2015-04-14 01:15:40 +0000
+++ hooks/charmhelpers/core/strutils.py 2015-09-22 06:12:36 +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 'hooks/nova_cc_utils.py'
--- hooks/nova_cc_utils.py 2015-07-22 12:01:24 +0000
+++ hooks/nova_cc_utils.py 2015-09-22 06:12:36 +0000
@@ -724,7 +724,10 @@
724def ssh_known_host_key(host, unit=None, user=None):724def ssh_known_host_key(host, unit=None, user=None):
725 cmd = ['ssh-keygen', '-f', known_hosts(unit, user), '-H', '-F', host]725 cmd = ['ssh-keygen', '-f', known_hosts(unit, user), '-H', '-F', host]
726 try:726 try:
727 return subprocess.check_output(cmd).strip()727 # The first line of output is like '# Host xx found: line 1 type RSA',
728 # which should be excluded.
729 output = subprocess.check_output(cmd).strip()
730 return output.split('\n')[1]
728 except subprocess.CalledProcessError:731 except subprocess.CalledProcessError:
729 return None732 return None
730733
@@ -735,6 +738,16 @@
735 subprocess.check_call(cmd)738 subprocess.check_call(cmd)
736739
737740
741def is_same_key(key_1, key_2):
742 # The key format get will be like '|1|2rUumCavEXWVaVyB5uMl6m85pZo=|Cp'
743 # 'EL6l7VTY37T/fg/ihhNb/GPgs= ssh-rsa AAAAB', we only need to compare
744 # the part start with 'ssh-rsa' followed with '= ', because the hash
745 # value in the beginning will change each time.
746 k_1 = key_1.split('= ')[1]
747 k_2 = key_2.split('= ')[1]
748 return k_1 == k_2
749
750
738def add_known_host(host, unit=None, user=None):751def add_known_host(host, unit=None, user=None):
739 '''Add variations of host to a known hosts file.'''752 '''Add variations of host to a known hosts file.'''
740 cmd = ['ssh-keyscan', '-H', '-t', 'rsa', host]753 cmd = ['ssh-keyscan', '-H', '-t', 'rsa', host]
@@ -745,8 +758,8 @@
745 raise e758 raise e
746759
747 current_key = ssh_known_host_key(host, unit, user)760 current_key = ssh_known_host_key(host, unit, user)
748 if current_key:761 if current_key and remote_key:
749 if remote_key == current_key:762 if is_same_key(remote_key, current_key):
750 log('Known host key for compute host %s up to date.' % host)763 log('Known host key for compute host %s up to date.' % host)
751 return764 return
752 else:765 else:
@@ -787,8 +800,7 @@
787 hosts.append(hn.split('.')[0])800 hosts.append(hn.split('.')[0])
788801
789 for host in list(set(hosts)):802 for host in list(set(hosts)):
790 if not ssh_known_host_key(host, unit, user):803 add_known_host(host, unit, user)
791 add_known_host(host, unit, user)
792804
793 if not ssh_authorized_key_exists(public_key, unit, user):805 if not ssh_authorized_key_exists(public_key, unit, user):
794 log('Saving SSH authorized key for compute host at %s.' %806 log('Saving SSH authorized key for compute host at %s.' %
795807
=== modified file 'tests/charmhelpers/contrib/amulet/deployment.py'
--- tests/charmhelpers/contrib/amulet/deployment.py 2015-03-31 11:39:19 +0000
+++ tests/charmhelpers/contrib/amulet/deployment.py 2015-09-22 06:12:36 +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-08-18 17:34:35 +0000
+++ tests/charmhelpers/contrib/amulet/utils.py 2015-09-22 06:12:36 +0000
@@ -114,7 +114,7 @@
114 # /!\ DEPRECATION WARNING (beisner):114 # /!\ DEPRECATION WARNING (beisner):
115 # New and existing tests should be rewritten to use115 # New and existing tests should be rewritten to use
116 # validate_services_by_name() as it is aware of init systems.116 # validate_services_by_name() as it is aware of init systems.
117 self.log.warn('/!\\ DEPRECATION WARNING: use '117 self.log.warn('DEPRECATION WARNING: use '
118 'validate_services_by_name instead of validate_services '118 'validate_services_by_name instead of validate_services '
119 'due to init system differences.')119 'due to init system differences.')
120120
@@ -269,33 +269,52 @@
269 """Get last modification time of directory."""269 """Get last modification time of directory."""
270 return sentry_unit.directory_stat(directory)['mtime']270 return sentry_unit.directory_stat(directory)['mtime']
271271
272 def _get_proc_start_time(self, sentry_unit, service, pgrep_full=False):272 def _get_proc_start_time(self, sentry_unit, service, pgrep_full=None):
273 """Get process' start time.273 """Get start time of a process based on the last modification time
274274 of the /proc/pid directory.
275 Determine start time of the process based on the last modification275
276 time of the /proc/pid directory. If pgrep_full is True, the process276 :sentry_unit: The sentry unit to check for the service on
277 name is matched against the full command line.277 :service: service name to look for in process table
278 """278 :pgrep_full: [Deprecated] Use full command line search mode with pgrep
279 if pgrep_full:279 :returns: epoch time of service process start
280 cmd = 'pgrep -o -f {}'.format(service)280 :param commands: list of bash commands
281 else:281 :param sentry_units: list of sentry unit pointers
282 cmd = 'pgrep -o {}'.format(service)282 :returns: None if successful; Failure message otherwise
283 cmd = cmd + ' | grep -v pgrep || exit 0'283 """
284 cmd_out = sentry_unit.run(cmd)284 if pgrep_full is not None:
285 self.log.debug('CMDout: ' + str(cmd_out))285 # /!\ DEPRECATION WARNING (beisner):
286 if cmd_out[0]:286 # No longer implemented, as pidof is now used instead of pgrep.
287 self.log.debug('Pid for %s %s' % (service, str(cmd_out[0])))287 # https://bugs.launchpad.net/charm-helpers/+bug/1474030
288 proc_dir = '/proc/{}'.format(cmd_out[0].strip())288 self.log.warn('DEPRECATION WARNING: pgrep_full bool is no '
289 return self._get_dir_mtime(sentry_unit, proc_dir)289 'longer implemented re: lp 1474030.')
290
291 pid_list = self.get_process_id_list(sentry_unit, service)
292 pid = pid_list[0]
293 proc_dir = '/proc/{}'.format(pid)
294 self.log.debug('Pid for {} on {}: {}'.format(
295 service, sentry_unit.info['unit_name'], pid))
296
297 return self._get_dir_mtime(sentry_unit, proc_dir)
290298
291 def service_restarted(self, sentry_unit, service, filename,299 def service_restarted(self, sentry_unit, service, filename,
292 pgrep_full=False, sleep_time=20):300 pgrep_full=None, sleep_time=20):
293 """Check if service was restarted.301 """Check if service was restarted.
294302
295 Compare a service's start time vs a file's last modification time303 Compare a service's start time vs a file's last modification time
296 (such as a config file for that service) to determine if the service304 (such as a config file for that service) to determine if the service
297 has been restarted.305 has been restarted.
298 """306 """
307 # /!\ DEPRECATION WARNING (beisner):
308 # This method is prone to races in that no before-time is known.
309 # Use validate_service_config_changed instead.
310
311 # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
312 # used instead of pgrep. pgrep_full is still passed through to ensure
313 # deprecation WARNS. lp1474030
314 self.log.warn('DEPRECATION WARNING: use '
315 'validate_service_config_changed instead of '
316 'service_restarted due to known races.')
317
299 time.sleep(sleep_time)318 time.sleep(sleep_time)
300 if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=319 if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=
301 self._get_file_mtime(sentry_unit, filename)):320 self._get_file_mtime(sentry_unit, filename)):
@@ -304,15 +323,15 @@
304 return False323 return False
305324
306 def service_restarted_since(self, sentry_unit, mtime, service,325 def service_restarted_since(self, sentry_unit, mtime, service,
307 pgrep_full=False, sleep_time=20,326 pgrep_full=None, sleep_time=20,
308 retry_count=2):327 retry_count=2, retry_sleep_time=30):
309 """Check if service was been started after a given time.328 """Check if service was been started after a given time.
310329
311 Args:330 Args:
312 sentry_unit (sentry): The sentry unit to check for the service on331 sentry_unit (sentry): The sentry unit to check for the service on
313 mtime (float): The epoch time to check against332 mtime (float): The epoch time to check against
314 service (string): service name to look for in process table333 service (string): service name to look for in process table
315 pgrep_full (boolean): Use full command line search mode with pgrep334 pgrep_full: [Deprecated] Use full command line search mode with pgrep
316 sleep_time (int): Seconds to sleep before looking for process335 sleep_time (int): Seconds to sleep before looking for process
317 retry_count (int): If service is not found, how many times to retry336 retry_count (int): If service is not found, how many times to retry
318337
@@ -321,30 +340,44 @@
321 False if service is older than mtime or if service was340 False if service is older than mtime or if service was
322 not found.341 not found.
323 """342 """
324 self.log.debug('Checking %s restarted since %s' % (service, mtime))343 # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
344 # used instead of pgrep. pgrep_full is still passed through to ensure
345 # deprecation WARNS. lp1474030
346
347 unit_name = sentry_unit.info['unit_name']
348 self.log.debug('Checking that %s service restarted since %s on '
349 '%s' % (service, mtime, unit_name))
325 time.sleep(sleep_time)350 time.sleep(sleep_time)
326 proc_start_time = self._get_proc_start_time(sentry_unit, service,351 proc_start_time = None
327 pgrep_full)352 tries = 0
328 while retry_count > 0 and not proc_start_time:353 while tries <= retry_count and not proc_start_time:
329 self.log.debug('No pid file found for service %s, will retry %i '354 try:
330 'more times' % (service, retry_count))355 proc_start_time = self._get_proc_start_time(sentry_unit,
331 time.sleep(30)356 service,
332 proc_start_time = self._get_proc_start_time(sentry_unit, service,357 pgrep_full)
333 pgrep_full)358 self.log.debug('Attempt {} to get {} proc start time on {} '
334 retry_count = retry_count - 1359 'OK'.format(tries, service, unit_name))
360 except IOError:
361 # NOTE(beisner) - race avoidance, proc may not exist yet.
362 # https://bugs.launchpad.net/charm-helpers/+bug/1474030
363 self.log.debug('Attempt {} to get {} proc start time on {} '
364 'failed'.format(tries, service, unit_name))
365 time.sleep(retry_sleep_time)
366 tries += 1
335367
336 if not proc_start_time:368 if not proc_start_time:
337 self.log.warn('No proc start time found, assuming service did '369 self.log.warn('No proc start time found, assuming service did '
338 'not start')370 'not start')
339 return False371 return False
340 if proc_start_time >= mtime:372 if proc_start_time >= mtime:
341 self.log.debug('proc start time is newer than provided mtime'373 self.log.debug('Proc start time is newer than provided mtime'
342 '(%s >= %s)' % (proc_start_time, mtime))374 '(%s >= %s) on %s (OK)' % (proc_start_time,
375 mtime, unit_name))
343 return True376 return True
344 else:377 else:
345 self.log.warn('proc start time (%s) is older than provided mtime '378 self.log.warn('Proc start time (%s) is older than provided mtime '
346 '(%s), service did not restart' % (proc_start_time,379 '(%s) on %s, service did not '
347 mtime))380 'restart' % (proc_start_time, mtime, unit_name))
348 return False381 return False
349382
350 def config_updated_since(self, sentry_unit, filename, mtime,383 def config_updated_since(self, sentry_unit, filename, mtime,
@@ -374,8 +407,9 @@
374 return False407 return False
375408
376 def validate_service_config_changed(self, sentry_unit, mtime, service,409 def validate_service_config_changed(self, sentry_unit, mtime, service,
377 filename, pgrep_full=False,410 filename, pgrep_full=None,
378 sleep_time=20, retry_count=2):411 sleep_time=20, retry_count=2,
412 retry_sleep_time=30):
379 """Check service and file were updated after mtime413 """Check service and file were updated after mtime
380414
381 Args:415 Args:
@@ -383,9 +417,10 @@
383 mtime (float): The epoch time to check against417 mtime (float): The epoch time to check against
384 service (string): service name to look for in process table418 service (string): service name to look for in process table
385 filename (string): The file to check mtime of419 filename (string): The file to check mtime of
386 pgrep_full (boolean): Use full command line search mode with pgrep420 pgrep_full: [Deprecated] Use full command line search mode with pgrep
387 sleep_time (int): Seconds to sleep before looking for process421 sleep_time (int): Initial sleep in seconds to pass to test helpers
388 retry_count (int): If service is not found, how many times to retry422 retry_count (int): If service is not found, how many times to retry
423 retry_sleep_time (int): Time in seconds to wait between retries
389424
390 Typical Usage:425 Typical Usage:
391 u = OpenStackAmuletUtils(ERROR)426 u = OpenStackAmuletUtils(ERROR)
@@ -402,15 +437,25 @@
402 mtime, False if service is older than mtime or if service was437 mtime, False if service is older than mtime or if service was
403 not found or if filename was modified before mtime.438 not found or if filename was modified before mtime.
404 """439 """
405 self.log.debug('Checking %s restarted since %s' % (service, mtime))440
406 time.sleep(sleep_time)441 # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
407 service_restart = self.service_restarted_since(sentry_unit, mtime,442 # used instead of pgrep. pgrep_full is still passed through to ensure
408 service,443 # deprecation WARNS. lp1474030
409 pgrep_full=pgrep_full,444
410 sleep_time=0,445 service_restart = self.service_restarted_since(
411 retry_count=retry_count)446 sentry_unit, mtime,
412 config_update = self.config_updated_since(sentry_unit, filename, mtime,447 service,
413 sleep_time=0)448 pgrep_full=pgrep_full,
449 sleep_time=sleep_time,
450 retry_count=retry_count,
451 retry_sleep_time=retry_sleep_time)
452
453 config_update = self.config_updated_since(
454 sentry_unit,
455 filename,
456 mtime,
457 sleep_time=0)
458
414 return service_restart and config_update459 return service_restart and config_update
415460
416 def get_sentry_time(self, sentry_unit):461 def get_sentry_time(self, sentry_unit):
@@ -428,7 +473,6 @@
428 """Return a list of all Ubuntu releases in order of release."""473 """Return a list of all Ubuntu releases in order of release."""
429 _d = distro_info.UbuntuDistroInfo()474 _d = distro_info.UbuntuDistroInfo()
430 _release_list = _d.all475 _release_list = _d.all
431 self.log.debug('Ubuntu release list: {}'.format(_release_list))
432 return _release_list476 return _release_list
433477
434 def file_to_url(self, file_rel_path):478 def file_to_url(self, file_rel_path):
435479
=== modified file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
--- tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-07-16 20:18:38 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-09-22 06:12:36 +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_nova_cc_utils.py'
--- unit_tests/test_nova_cc_utils.py 2015-06-10 15:48:34 +0000
+++ unit_tests/test_nova_cc_utils.py 2015-09-22 06:12:36 +0000
@@ -416,8 +416,8 @@
416 @patch.object(utils, 'ssh_known_host_key')416 @patch.object(utils, 'ssh_known_host_key')
417 @patch('subprocess.check_output')417 @patch('subprocess.check_output')
418 def test_add_known_host_exists(self, check_output, host_key, rm):418 def test_add_known_host_exists(self, check_output, host_key, rm):
419 check_output.return_value = 'fookey'419 check_output.return_value = '|1|= fookey'
420 host_key.return_value = 'fookey'420 host_key.return_value = '|1|= fookey'
421 with patch_open() as (_open, _file):421 with patch_open() as (_open, _file):
422 utils.add_known_host('foohost')422 utils.add_known_host('foohost')
423 self.assertFalse(rm.called)423 self.assertFalse(rm.called)
@@ -429,8 +429,8 @@
429 @patch('subprocess.check_output')429 @patch('subprocess.check_output')
430 def test_add_known_host_exists_outdated(430 def test_add_known_host_exists_outdated(
431 self, check_output, host_key, rm, known_hosts):431 self, check_output, host_key, rm, known_hosts):
432 check_output.return_value = 'fookey'432 check_output.return_value = '|1|= fookey'
433 host_key.return_value = 'fookey_old'433 host_key.return_value = '|1|= fookey_old'
434 with patch_open() as (_open, _file):434 with patch_open() as (_open, _file):
435 utils.add_known_host('foohost', None, None)435 utils.add_known_host('foohost', None, None)
436 rm.assert_called_with('foohost', None, None)436 rm.assert_called_with('foohost', None, None)
@@ -441,13 +441,13 @@
441 @patch('subprocess.check_output')441 @patch('subprocess.check_output')
442 def test_add_known_host_exists_added(442 def test_add_known_host_exists_added(
443 self, check_output, host_key, rm, known_hosts):443 self, check_output, host_key, rm, known_hosts):
444 check_output.return_value = 'fookey'444 check_output.return_value = '|1|= fookey'
445 host_key.return_value = None445 host_key.return_value = None
446 with patch_open() as (_open, _file):446 with patch_open() as (_open, _file):
447 _file.write = MagicMock()447 _file.write = MagicMock()
448 utils.add_known_host('foohost')448 utils.add_known_host('foohost')
449 self.assertFalse(rm.called)449 self.assertFalse(rm.called)
450 _file.write.assert_called_with('fookey\n')450 _file.write.assert_called_with('|1|= fookey\n')
451451
452 @patch('__builtin__.open')452 @patch('__builtin__.open')
453 @patch('os.mkdir')453 @patch('os.mkdir')

Subscribers

People subscribed via source and target branches