~andrewjbeach/juju-ci-tools/make-local-patcher

« back to all changes in this revision

Viewing changes to deploy_stack.py

  • Committer: Curtis Hovey
  • Date: 2016-05-25 20:59:08 UTC
  • Revision ID: curtis@canonical.com-20160525205908-1yndw393rgkmxxcc
Revert git_gate.py change.

Show diffs side-by-side

added added

removed removed

Lines of Context:
7
7
    contextmanager,
8
8
    nested,
9
9
)
10
 
from datetime import (
11
 
    datetime,
12
 
)
13
 
 
14
 
import errno
15
10
import glob
16
11
import logging
17
12
import os
25
20
import shutil
26
21
 
27
22
from chaos import background_chaos
28
 
from fakejuju import (
29
 
    FakeBackend,
30
 
    fake_juju_client,
31
 
)
32
23
from jujucharm import (
33
24
    local_charm_path,
34
25
)
38
29
    translate_to_env,
39
30
)
40
31
from jujupy import (
41
 
    client_from_config,
 
32
    EnvJujuClient,
42
33
    get_local_root,
43
34
    get_machine_dns_name,
44
35
    jes_home_path,
108
99
        # finished; two machines initializing concurrently may
109
100
        # need even 40 minutes. In addition Windows image blobs or
110
101
        # any system deployment using MAAS requires extra time.
111
 
        client.wait_for_started(7200)
112
 
    else:
113
102
        client.wait_for_started(3600)
 
103
    else:
 
104
        client.wait_for_started()
114
105
 
115
106
 
116
107
def assess_juju_relations(client):
133
124
    """
134
125
 
135
126
 
136
 
def get_token_from_status(client):
137
 
    """Return the token from the application status message or None."""
138
 
    status = client.get_status()
139
 
    unit = status.get_unit('dummy-sink/0')
140
 
    app_status = unit.get('workload-status')
141
 
    if app_status is not None:
142
 
        message = app_status.get('message', '')
143
 
        parts = message.split()
144
 
        if parts:
145
 
            return parts[-1]
146
 
    return None
147
 
 
148
 
 
149
127
def check_token(client, token, timeout=120):
150
 
    """Check the token found on dummy-sink/0 or raise ValueError."""
151
 
    logging.info('Waiting for applications to reach ready.')
152
 
    client.wait_for_workloads()
153
128
    # Wait up to 120 seconds for token to be created.
 
129
    # Utopic is slower, maybe because the devel series gets more
 
130
    # package updates.
154
131
    logging.info('Retrieving token.')
155
132
    remote = remote_from_unit(client, "dummy-sink/0")
156
133
    # Update remote with real address if needed.
158
135
    start = time.time()
159
136
    while True:
160
137
        if remote.is_windows():
161
 
            result = get_token_from_status(client)
162
 
            if not result:
163
 
                try:
164
 
                    result = remote.cat("%ProgramData%\\dummy-sink\\token")
165
 
                except winrm.exceptions.WinRMTransportError as e:
166
 
                    logging.warning(
167
 
                        "Skipping token check because of: {}".format(str(e)))
168
 
                    return
 
138
            try:
 
139
                result = remote.cat("%ProgramData%\\dummy-sink\\token")
 
140
            except winrm.exceptions.WinRMTransportError as e:
 
141
                print("Skipping token check because of: {}".format(str(e)))
169
142
        else:
170
143
            result = remote.run(GET_TOKEN_SCRIPT)
171
144
        token_pattern = re.compile(r'([^\n\r]*)\r?\n?')
296
269
    """Compress log files in given log_dir using gzip."""
297
270
    log_files = []
298
271
    for r, ds, fs in os.walk(log_dir):
299
 
        log_files.extend(os.path.join(r, f) for f in fs if is_log(f))
 
272
        log_files.extend(os.path.join(r, f) for f in fs if f.endswith(".log"))
300
273
    if log_files:
301
274
        subprocess.check_call(['gzip', '--best', '-f'] + log_files)
302
275
 
303
276
 
304
 
def is_log(file_name):
305
 
    """Check to see if the given file name is the name of a log file."""
306
 
    return file_name.endswith('.log') or file_name.endswith('syslog')
307
 
 
308
 
 
309
277
lxc_template_glob = '/var/lib/juju/containers/juju-*-lxc-template/*.log'
310
278
 
311
279
 
337
305
            '/var/log/juju/*.log',
338
306
            # TODO(gz): Also capture kvm container logs?
339
307
            '/var/lib/juju/containers/juju-*-lxc-*/',
340
 
            '/var/log/lxd/juju-*',
341
 
            '/var/log/lxd/lxd.log',
342
308
            '/var/log/syslog',
343
309
            '/var/log/mongodb/mongodb.log',
344
 
            '/etc/network/interfaces',
345
 
            '/home/ubuntu/ifconfig.log',
346
310
        ]
347
311
 
348
312
        try:
357
321
            # The juju log dir is not created until after cloud-init succeeds.
358
322
            logging.warning("Could not allow access to the juju logs:")
359
323
            logging.warning(e.output)
360
 
        try:
361
 
            remote.run('ifconfig > /home/ubuntu/ifconfig.log')
362
 
        except subprocess.CalledProcessError as e:
363
 
            logging.warning("Could not capture ifconfig state:")
364
 
            logging.warning(e.output)
365
324
 
366
325
    try:
367
326
        remote.copy(directory, log_paths)
376
335
 
377
336
 
378
337
def assess_juju_run(client):
379
 
    responses = client.run(('uname',), ['dummy-source', 'dummy-sink'])
 
338
    responses = client.get_juju_output('run', '--format', 'json', '--service',
 
339
                                       'dummy-source,dummy-sink', 'uname')
 
340
    responses = json.loads(responses)
380
341
    for machine in responses:
381
342
        if machine.get('ReturnCode', 0) != 0:
382
343
            raise ValueError('juju run on machine %s returned %d: %s' % (
390
351
 
391
352
 
392
353
def assess_upgrade(old_client, juju_path):
393
 
    all_clients = _get_clients_to_upgrade(old_client, juju_path)
394
 
 
395
 
    # all clients have the same provider type, work this out once.
396
 
    if all_clients[0].env.config['type'] == 'maas':
 
354
    client = EnvJujuClient.by_version(old_client.env, juju_path,
 
355
                                      old_client.debug)
 
356
    upgrade_juju(client)
 
357
    if client.env.config['type'] == 'maas':
397
358
        timeout = 1200
398
359
    else:
399
360
        timeout = 600
400
 
 
401
 
    for client in all_clients:
402
 
        upgrade_juju(client)
403
 
        client.wait_for_version(client.get_matching_agent_version(), timeout)
404
 
 
405
 
 
406
 
def _get_clients_to_upgrade(old_client, juju_path):
407
 
    """Return a list of cloned clients to upgrade.
408
 
 
409
 
    Ensure that the controller (if available) is the first client in the list.
410
 
    """
411
 
    new_client = old_client.clone_path_cls(juju_path)
412
 
    all_clients = sorted(
413
 
        new_client.iter_model_clients(),
414
 
        key=lambda m: m.model_name == 'controller',
415
 
        reverse=True)
416
 
 
417
 
    return all_clients
 
361
    client.wait_for_version(client.get_matching_agent_version(), timeout)
418
362
 
419
363
 
420
364
def upgrade_juju(client):
421
 
    client.set_testing_agent_metadata_url()
422
 
    tools_metadata_url = client.get_agent_metadata_url()
423
 
    logging.info(
424
 
        'The {url_type} is {url}'.format(
425
 
            url_type=client.agent_metadata_url,
426
 
            url=tools_metadata_url))
 
365
    client.set_testing_tools_metadata_url()
 
366
    tools_metadata_url = client.get_env_option('tools-metadata-url')
 
367
    logging.info('The tools-metadata-url is %s', tools_metadata_url)
427
368
    client.upgrade_juju()
428
369
 
429
370
 
536
477
            self.known_hosts['0'] = bootstrap_host
537
478
 
538
479
    @classmethod
539
 
    def _generate_default_clean_dir(cls, temp_env_name):
540
 
        """Creates a new unique directory for logging and returns name"""
541
 
        logging.info('Environment {}'.format(temp_env_name))
542
 
        test_name = temp_env_name.split('-')[0]
543
 
        timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
544
 
        log_dir = os.path.join('/tmp', test_name, 'logs', timestamp)
545
 
 
546
 
        try:
547
 
            os.makedirs(log_dir)
548
 
            logging.info('Created logging directory {}'.format(log_dir))
549
 
        except OSError as e:
550
 
            if e.errno == errno.EEXIST:
551
 
                logging.warn('"Directory {} already exists'.format(log_dir))
552
 
            else:
553
 
                raise('Failed to create logging directory: {} ' +
554
 
                      log_dir +
555
 
                      '. Please specify empty folder or try again')
556
 
        return log_dir
557
 
 
558
 
    @classmethod
559
480
    def from_args(cls, args):
560
 
        if not args.logs:
561
 
            args.logs = cls._generate_default_clean_dir(args.temp_env_name)
562
 
 
563
 
        # GZ 2016-08-11: Move this logic into client_from_config maybe?
564
 
        if args.juju_bin == 'FAKE':
565
 
            env = SimpleEnvironment.from_config(args.env)
566
 
            client = fake_juju_client(env=env)
567
 
        else:
568
 
            client = client_from_config(args.env, args.juju_bin,
569
 
                                        debug=args.debug,
570
 
                                        soft_deadline=args.deadline)
 
481
        env = SimpleEnvironment.from_config(args.env)
 
482
        client = EnvJujuClient.by_version(env, args.juju_bin, debug=args.debug)
571
483
        jes_enabled = client.is_jes_enabled()
572
484
        return cls(
573
485
            args.temp_env_name, client, client, args.bootstrap_host,
600
512
            else:
601
513
                yield self.machines
602
514
        finally:
 
515
            # Although this isn't MAAS-related, it was in this context in
 
516
            # boot_context.
 
517
            logging.info(
 
518
                'Juju command timings: {}'.format(
 
519
                    self.client.get_juju_timings()))
 
520
            dump_juju_timings(self.client, self.log_dir)
603
521
            if self.client.env.config['type'] == 'maas' and not self.keep_env:
604
522
                logging.info("Waiting for destroy-environment to complete")
605
523
                time.sleep(90)
646
564
            raise AssertionError('Tear down client needs same env!')
647
565
        tear_down(self.tear_down_client, jes_enabled, try_jes=try_jes)
648
566
 
649
 
    def _log_and_wrap_exception(self, exc):
650
 
        logging.exception(exc)
651
 
        stdout = getattr(exc, 'output', None)
652
 
        stderr = getattr(exc, 'stderr', None)
653
 
        if stdout or stderr:
654
 
            logging.info(
655
 
                'Output from exception:\nstdout:\n%s\nstderr:\n%s',
656
 
                stdout, stderr)
657
 
        return LoggedException(exc)
658
 
 
659
567
    @contextmanager
660
568
    def bootstrap_context(self, machines, omit_config=None):
661
569
        """Context for bootstrapping a state server."""
695
603
        ensure_deleted(jenv_path)
696
604
        with temp_bootstrap_env(self.client.env.juju_home, self.client,
697
605
                                permanent=self.permanent, set_home=False):
698
 
            with self.handle_bootstrap_exceptions():
699
 
                if not torn_down:
700
 
                    self.tear_down(try_jes=True)
701
 
                yield
702
 
 
703
 
    @contextmanager
704
 
    def existing_bootstrap_context(self, machines, omit_config=None):
705
 
        """ Context for bootstrapping a state server that shares the
706
 
        environment with an existing bootstrap environment.
707
 
 
708
 
        Using this context makes it possible to boot multiple simultaneous
709
 
        environments that share a JUJU_HOME.
710
 
 
711
 
        """
712
 
        bootstrap_host = self.known_hosts.get('0')
713
 
        kwargs = dict(
714
 
            series=self.series, bootstrap_host=bootstrap_host,
715
 
            agent_url=self.agent_url, agent_stream=self.agent_stream,
716
 
            region=self.region)
717
 
        if omit_config is not None:
718
 
            for key in omit_config:
719
 
                kwargs.pop(key.replace('-', '_'), None)
720
 
        update_env(self.client.env, self.temp_env_name, **kwargs)
721
 
        ssh_machines = list(machines)
722
 
        if bootstrap_host is not None:
723
 
            ssh_machines.append(bootstrap_host)
724
 
        for machine in ssh_machines:
725
 
            logging.info('Waiting for port 22 on %s' % machine)
726
 
            wait_for_port(machine, 22, timeout=120)
727
 
 
728
 
        with self.handle_bootstrap_exceptions():
729
 
            yield
730
 
 
731
 
    @contextmanager
732
 
    def handle_bootstrap_exceptions(self):
733
 
        """If an exception is raised during bootstrap, handle it.
734
 
 
735
 
        Log the exception, re-raise as a LoggedException.
736
 
        Copy logs for the bootstrap host
737
 
        Tear down.  (self.keep_env is ignored.)
738
 
        """
739
 
        try:
740
606
            try:
741
 
                yield
742
 
            # If an exception is raised that indicates an error, log it
743
 
            # before tearing down so that the error is closely tied to
744
 
            # the failed operation.
745
 
            except Exception as e:
746
 
                raise self._log_and_wrap_exception(e)
747
 
        except:
748
 
            # If run from a windows machine may not have ssh to get
749
 
            # logs
750
 
            with self.client.ignore_soft_deadline():
751
 
                with self.tear_down_client.ignore_soft_deadline():
752
 
                    if self.bootstrap_host is not None and _can_run_ssh():
753
 
                        remote = remote_from_address(self.bootstrap_host,
754
 
                                                     series=self.series)
755
 
                        copy_remote_logs(remote, self.log_dir)
756
 
                        archive_logs(self.log_dir)
757
 
                    self.tear_down()
758
 
            raise
 
607
                try:
 
608
                    if not torn_down:
 
609
                        self.tear_down(try_jes=True)
 
610
                    yield
 
611
                # If an exception is raised that indicates an error, log it
 
612
                # before tearing down so that the error is closely tied to
 
613
                # the failed operation.
 
614
                except Exception as e:
 
615
                    logging.exception(e)
 
616
                    if getattr(e, 'output', None):
 
617
                        print_now('\n')
 
618
                        print_now(e.output)
 
619
                    raise LoggedException(e)
 
620
            except:
 
621
                # If run from a windows machine may not have ssh to get
 
622
                # logs
 
623
                if self.bootstrap_host is not None and _can_run_ssh():
 
624
                    remote = remote_from_address(self.bootstrap_host,
 
625
                                                 series=self.series)
 
626
                    copy_remote_logs(remote, self.log_dir)
 
627
                    archive_logs(self.log_dir)
 
628
                self.tear_down()
 
629
                raise
759
630
 
760
631
    @contextmanager
761
632
    def runtime_context(self, addable_machines):
767
638
        try:
768
639
            try:
769
640
                if len(self.known_hosts) == 0:
770
 
                    host = get_machine_dns_name(
771
 
                        self.client.get_controller_client(), '0')
 
641
                    host = get_machine_dns_name(self.client.get_admin_client(),
 
642
                                                '0')
772
643
                    if host is None:
773
644
                        raise ValueError('Could not get machine 0 host')
774
645
                    self.known_hosts['0'] = host
779
650
            except GeneratorExit:
780
651
                raise
781
652
            except BaseException as e:
782
 
                raise self._log_and_wrap_exception(e)
 
653
                logging.exception(e)
 
654
                raise LoggedException(e)
783
655
        except:
784
656
            safe_print_status(self.client)
785
657
            raise
786
658
        else:
787
 
            with self.client.ignore_soft_deadline():
788
 
                self.client.list_controllers()
789
 
                self.client.list_models()
790
 
                for m_client in self.client.iter_model_clients():
791
 
                    m_client.show_status()
 
659
            self.client.show_status()
792
660
        finally:
793
 
            with self.client.ignore_soft_deadline():
794
 
                with self.tear_down_client.ignore_soft_deadline():
795
 
                    try:
796
 
                        self.dump_all_logs()
797
 
                    except KeyboardInterrupt:
798
 
                        pass
799
 
                    if not self.keep_env:
800
 
                        self.tear_down(self.jes_enabled)
801
 
 
802
 
    # GZ 2016-08-11: Should this method be elsewhere to avoid poking backend?
803
 
    def _should_dump(self):
804
 
        return not isinstance(self.client._backend, FakeBackend)
 
661
            try:
 
662
                self.dump_all_logs()
 
663
            except KeyboardInterrupt:
 
664
                pass
 
665
            if not self.keep_env:
 
666
                self.tear_down(self.jes_enabled)
805
667
 
806
668
    def dump_all_logs(self):
807
669
        """Dump logs for all models in the bootstrapped controller."""
808
670
        # This is accurate because we bootstrapped self.client.  It might not
809
671
        # be accurate for a model created by create_environment.
810
 
        if not self._should_dump():
811
 
            return
812
 
        controller_client = self.client.get_controller_client()
 
672
        admin_client = self.client.get_admin_client()
813
673
        if not self.jes_enabled:
814
674
            clients = [self.client]
815
675
        else:
817
677
                clients = list(self.client.iter_model_clients())
818
678
            except Exception:
819
679
                # Even if the controller is unreachable, we may still be able
820
 
                # to gather some logs. The controller_client and self.client
821
 
                # instances are all we have knowledge of.
822
 
                clients = [controller_client]
823
 
                if self.client is not controller_client:
 
680
                # to gather some logs.  admin_client and self.client are all
 
681
                # we have knowledge of.
 
682
                clients = [admin_client]
 
683
                if self.client is not admin_client:
824
684
                    clients.append(self.client)
825
685
        for client in clients:
826
 
            if client.env.environment == controller_client.env.environment:
 
686
            if client.env.environment == admin_client.env.environment:
827
687
                known_hosts = self.known_hosts
828
688
                if self.jes_enabled:
829
689
                    runtime_config = self.client.get_cache_path()
844
704
        """Context for running all juju operations in."""
845
705
        with self.maas_machines() as machines:
846
706
            with self.aws_machines() as new_machines:
847
 
                try:
848
 
                    yield machines + new_machines
849
 
                finally:
850
 
                    # This is not done in dump_all_logs because it should be
851
 
                    # done after tear down.
852
 
                    if self.log_dir is not None:
853
 
                        dump_juju_timings(self.client, self.log_dir)
 
707
                yield machines + new_machines
854
708
 
855
709
    @contextmanager
856
 
    def booted_context(self, upload_tools, **kwargs):
 
710
    def booted_context(self, upload_tools):
857
711
        """Create a temporary environment in a context manager to run tests in.
858
712
 
859
713
        Bootstrap a new environment from a temporary config that is suitable
866
720
 
867
721
        :param upload_tools: False or True to upload the local agent instead
868
722
            of using streams.
869
 
        :param **kwargs: All remaining keyword arguments are passed to the
870
 
        client's bootstrap.
871
723
        """
872
724
        try:
873
725
            with self.top_context() as machines:
874
726
                with self.bootstrap_context(
875
727
                        machines, omit_config=self.client.bootstrap_replaces):
876
728
                    self.client.bootstrap(
877
 
                        upload_tools=upload_tools,
878
 
                        bootstrap_series=self.series,
879
 
                        **kwargs)
880
 
                with self.runtime_context(machines):
881
 
                    self.client.list_controllers()
882
 
                    self.client.list_models()
883
 
                    for m_client in self.client.iter_model_clients():
884
 
                        m_client.show_status()
885
 
                    yield machines
886
 
        except LoggedException:
887
 
            sys.exit(1)
888
 
 
889
 
    @contextmanager
890
 
    def existing_booted_context(self, upload_tools, **kwargs):
891
 
        try:
892
 
            with self.top_context() as machines:
893
 
                # Existing does less things as there is no pre-cleanup needed.
894
 
                with self.existing_bootstrap_context(
895
 
                        machines, omit_config=self.client.bootstrap_replaces):
896
 
                    self.client.bootstrap(
897
 
                        upload_tools=upload_tools,
898
 
                        bootstrap_series=self.series,
899
 
                        **kwargs)
 
729
                        upload_tools, bootstrap_series=self.series)
900
730
                with self.runtime_context(machines):
901
731
                    yield machines
902
732
        except LoggedException:
951
781
        sys.path = [p for p in sys.path if 'OpenSSH' not in p]
952
782
    # GZ 2016-01-22: When upgrading, could make sure to tear down with the
953
783
    # newer client instead, this will be required for major version upgrades?
954
 
    client = client_from_config(args.env, start_juju_path, args.debug,
955
 
                                soft_deadline=args.deadline)
 
784
    client = EnvJujuClient.by_version(
 
785
        SimpleEnvironment.from_config(args.env), start_juju_path, args.debug)
956
786
    if args.jes and not client.is_jes_enabled():
957
787
        client.enable_jes()
958
788
    jes_enabled = client.is_jes_enabled()
965
795
            # The win and osx client tests only verify the client
966
796
            # can bootstrap and call the state-server.
967
797
            return
 
798
        client.show_status()
968
799
        if args.with_chaos > 0:
969
800
            manager = background_chaos(args.temp_env_name, client,
970
801
                                       args.logs, args.with_chaos)
989
820
def safe_print_status(client):
990
821
    """Show the output of juju status without raising exceptions."""
991
822
    try:
992
 
        for m_client in client.iter_model_clients():
993
 
            m_client.show_status()
 
823
        client.show_status()
994
824
    except Exception as e:
995
825
        logging.exception(e)
996
826
 
997
827
 
998
 
def wait_for_state_server_to_shutdown(host, client, instance_id, timeout=60):
 
828
def wait_for_state_server_to_shutdown(host, client, instance_id):
999
829
    print_now("Waiting for port to close on %s" % host)
1000
 
    wait_for_port(host, 17070, closed=True, timeout=timeout)
 
830
    wait_for_port(host, 17070, closed=True)
1001
831
    print_now("Closed.")
1002
832
    provider_type = client.env.config.get('type')
1003
833
    if provider_type == 'openstack':