~ubuntu-branches/debian/stretch/waagent/stretch

« back to all changes in this revision

Viewing changes to azurelinuxagent/ga/update.py

  • Committer: Package Import Robot
  • Author(s): Bastian Blank
  • Date: 2016-08-24 16:48:22 UTC
  • mfrom: (1.2.5)
  • Revision ID: package-import@ubuntu.com-20160824164822-vdf8m5xy5gycm1cz
Tags: 2.1.6-1
New upstream version.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Windows Azure Linux Agent
 
2
#
 
3
# Copyright 2014 Microsoft Corporation
 
4
#
 
5
# Licensed under the Apache License, Version 2.0 (the "License");
 
6
# you may not use this file except in compliance with the License.
 
7
# You may obtain a copy of the License at
 
8
#
 
9
#     http://www.apache.org/licenses/LICENSE-2.0
 
10
#
 
11
# Unless required by applicable law or agreed to in writing, software
 
12
# distributed under the License is distributed on an "AS IS" BASIS,
 
13
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 
14
# See the License for the specific language governing permissions and
 
15
# limitations under the License.
 
16
#
 
17
# Requires Python 2.4+ and Openssl 1.0+
 
18
#
 
19
import glob
 
20
import json
 
21
import os
 
22
import platform
 
23
import re
 
24
import shlex
 
25
import shutil
 
26
import signal
 
27
import subprocess
 
28
import sys
 
29
import time
 
30
import zipfile
 
31
 
 
32
import azurelinuxagent.common.conf as conf
 
33
import azurelinuxagent.common.logger as logger
 
34
import azurelinuxagent.common.utils.fileutil as fileutil
 
35
import azurelinuxagent.common.utils.restutil as restutil
 
36
import azurelinuxagent.common.utils.textutil as textutil
 
37
 
 
38
from azurelinuxagent.common.event import add_event, WALAEventOperation
 
39
from azurelinuxagent.common.exception import UpdateError, ProtocolError
 
40
from azurelinuxagent.common.future import ustr
 
41
from azurelinuxagent.common.osutil import get_osutil
 
42
from azurelinuxagent.common.protocol import get_protocol_util
 
43
from azurelinuxagent.common.utils.flexible_version import FlexibleVersion
 
44
from azurelinuxagent.common.version import AGENT_NAME, AGENT_VERSION, AGENT_LONG_VERSION, \
 
45
                                            AGENT_DIR_GLOB, AGENT_PKG_GLOB, \
 
46
                                            AGENT_PATTERN, AGENT_NAME_PATTERN, AGENT_DIR_PATTERN, \
 
47
                                            CURRENT_AGENT, CURRENT_VERSION, \
 
48
                                            is_current_agent_installed
 
49
 
 
50
from azurelinuxagent.ga.exthandlers import HandlerManifest
 
51
 
 
52
 
 
53
AGENT_ERROR_FILE = "error.json" # File name for agent error record
 
54
AGENT_MANIFEST_FILE = "HandlerManifest.json"
 
55
 
 
56
CHILD_HEALTH_INTERVAL = 15 * 60
 
57
CHILD_LAUNCH_INTERVAL = 5 * 60
 
58
CHILD_LAUNCH_RESTART_MAX = 3
 
59
CHILD_POLL_INTERVAL = 60
 
60
 
 
61
MAX_FAILURE = 3 # Max failure allowed for agent before blacklisted
 
62
RETAIN_INTERVAL = 24 * 60 * 60 # Retain interval for black list
 
63
 
 
64
GOAL_STATE_INTERVAL = 25
 
65
REPORT_STATUS_INTERVAL = 15
 
66
 
 
67
ORPHAN_WAIT_INTERVAL = 15 * 60 * 60
 
68
 
 
69
AGENT_SENTINAL_FILE = "current_version"
 
70
 
 
71
 
 
72
def get_update_handler():
 
73
    return UpdateHandler()
 
74
 
 
75
 
 
76
def get_python_cmd():
 
77
    major_version = platform.python_version_tuple()[0]
 
78
    return "python" if int(major_version) <= 2 else "python{0}".format(major_version)
 
79
 
 
80
 
 
81
class UpdateHandler(object):
 
82
 
 
83
    def __init__(self):
 
84
        self.osutil = get_osutil()
 
85
        self.protocol_util = get_protocol_util()
 
86
 
 
87
        self.running = True
 
88
        self.last_etag = None
 
89
        self.last_attempt_time = None
 
90
 
 
91
        self.agents = []
 
92
 
 
93
        self.child_agent = None
 
94
        self.child_launch_time = None
 
95
        self.child_launch_attempts = 0
 
96
        self.child_process = None
 
97
 
 
98
        self.signal_handler = None
 
99
        return
 
100
 
 
101
    def run_latest(self):
 
102
        """
 
103
        This method is called from the daemon to find and launch the most
 
104
        current, downloaded agent.
 
105
 
 
106
        Note:
 
107
        - Most events should be tagged to the launched agent (agent_version)
 
108
        """
 
109
 
 
110
        if self.child_process is not None:
 
111
            raise Exception("Illegal attempt to launch multiple goal state Agent processes")
 
112
 
 
113
        if self.signal_handler is None:
 
114
            self.signal_handler = signal.signal(signal.SIGTERM, self.forward_signal)
 
115
 
 
116
        latest_agent = self.get_latest_agent()
 
117
        if latest_agent is None:
 
118
            logger.info(u"Installed Agent {0} is the most current agent", CURRENT_AGENT)
 
119
            agent_cmd = "python -u {0} -run-exthandlers".format(sys.argv[0])
 
120
            agent_dir = os.getcwd()
 
121
            agent_name = CURRENT_AGENT
 
122
            agent_version = CURRENT_VERSION
 
123
        else:
 
124
            logger.info(u"Determined Agent {0} to be the latest agent", latest_agent.name)
 
125
            agent_cmd = latest_agent.get_agent_cmd()
 
126
            agent_dir = latest_agent.get_agent_dir()
 
127
            agent_name = latest_agent.name
 
128
            agent_version = latest_agent.version
 
129
 
 
130
        try:
 
131
 
 
132
            # Launch the correct Python version for python-based agents
 
133
            cmds = shlex.split(agent_cmd)
 
134
            if cmds[0].lower() == "python":
 
135
                cmds[0] = get_python_cmd()
 
136
                agent_cmd = " ".join(cmds)
 
137
 
 
138
            self._evaluate_agent_health(latest_agent)
 
139
 
 
140
            self.child_process = subprocess.Popen(
 
141
                cmds,
 
142
                cwd=agent_dir,
 
143
                stdout=sys.stdout,
 
144
                stderr=sys.stderr)
 
145
 
 
146
            logger.info(u"Agent {0} launched with command '{1}'", agent_name, agent_cmd)
 
147
 
 
148
            ret = None
 
149
            start_time = time.time()
 
150
            while (time.time() - start_time) < CHILD_HEALTH_INTERVAL:
 
151
                time.sleep(CHILD_POLL_INTERVAL)
 
152
                ret = self.child_process.poll()
 
153
                if ret is not None:
 
154
                    break
 
155
 
 
156
            if ret is None or ret <= 0:
 
157
                msg = u"Agent {0} launched with command '{1}' is successfully running".format(
 
158
                    agent_name,
 
159
                    agent_cmd)
 
160
                logger.info(msg)
 
161
                add_event(
 
162
                    AGENT_NAME,
 
163
                    version=agent_version,
 
164
                    op=WALAEventOperation.Enable,
 
165
                    is_success=True,
 
166
                    message=msg)
 
167
 
 
168
                if ret is None:
 
169
                    ret = self.child_process.wait()
 
170
 
 
171
            else:
 
172
                msg = u"Agent {0} launched with command '{1}' failed with return code: {2}".format(
 
173
                    agent_name,
 
174
                    agent_cmd,
 
175
                    ret)
 
176
                logger.warn(msg)
 
177
                add_event(
 
178
                    AGENT_NAME,
 
179
                    version=agent_version,
 
180
                    op=WALAEventOperation.Enable,
 
181
                    is_success=False,
 
182
                    message=msg)
 
183
 
 
184
            if ret is not None and ret > 0:
 
185
                msg = u"Agent {0} launched with command '{1}' returned code: {2}".format(
 
186
                    agent_name,
 
187
                    agent_cmd,
 
188
                    ret)
 
189
                logger.warn(msg)
 
190
                if latest_agent is not None:
 
191
                    latest_agent.mark_failure()
 
192
 
 
193
        except Exception as e:
 
194
            msg = u"Agent {0} launched with command '{1}' failed with exception: {2}".format(
 
195
                agent_name,
 
196
                agent_cmd,
 
197
                ustr(e))
 
198
            logger.warn(msg)
 
199
            add_event(
 
200
                AGENT_NAME,
 
201
                version=agent_version,
 
202
                op=WALAEventOperation.Enable,
 
203
                is_success=False,
 
204
                message=msg)
 
205
            if latest_agent is not None:
 
206
                latest_agent.mark_failure(is_fatal=True)
 
207
 
 
208
        self.child_process = None
 
209
        return
 
210
 
 
211
    def run(self):
 
212
        """
 
213
        This is the main loop which watches for agent and extension updates.
 
214
        """
 
215
 
 
216
        logger.info(u"Agent {0} is running as the goal state agent", CURRENT_AGENT)
 
217
 
 
218
        # Launch monitoring threads
 
219
        from azurelinuxagent.ga.monitor import get_monitor_handler
 
220
        get_monitor_handler().run()
 
221
 
 
222
        from azurelinuxagent.ga.env import get_env_handler
 
223
        get_env_handler().run()
 
224
 
 
225
        from azurelinuxagent.ga.exthandlers import get_exthandlers_handler, migrate_handler_state
 
226
        exthandlers_handler = get_exthandlers_handler()
 
227
        migrate_handler_state()
 
228
 
 
229
        try:
 
230
            self._ensure_no_orphans()
 
231
            self._emit_restart_event()
 
232
 
 
233
            # TODO: Add means to stop running
 
234
            while self.running:
 
235
                if self._is_orphaned:
 
236
                    logger.info("Goal state agent {0} was orphaned -- exiting", CURRENT_AGENT)
 
237
                    break
 
238
 
 
239
                if self._ensure_latest_agent():
 
240
                    if len(self.agents) > 0:
 
241
                        logger.info(
 
242
                            u"Agent {0} discovered {1} as an update and will exit",
 
243
                            CURRENT_AGENT,
 
244
                            self.agents[0].name)
 
245
                    break
 
246
 
 
247
                exthandlers_handler.run()
 
248
                
 
249
                time.sleep(GOAL_STATE_INTERVAL)
 
250
 
 
251
        except Exception as e:
 
252
            logger.warn(u"Agent {0} failed with exception: {1}", CURRENT_AGENT, ustr(e))
 
253
            sys.exit(1)
 
254
            return
 
255
 
 
256
        self._shutdown()
 
257
        sys.exit(0)
 
258
        return
 
259
 
 
260
    def forward_signal(self, signum, frame):
 
261
        # Note:
 
262
        #  - At present, the handler is registered only for SIGTERM.
 
263
        #    However, clean shutdown is both SIGTERM and SIGKILL.
 
264
        #    A SIGKILL handler is not being registered at this time to
 
265
        #    minimize perturbing the code.
 
266
        if signum in (signal.SIGTERM, signal.SIGKILL):
 
267
            self._shutdown()
 
268
 
 
269
        if self.child_process is None:
 
270
            return
 
271
        
 
272
        logger.info(
 
273
            u"Agent {0} forwarding signal {1} to {2}",
 
274
            CURRENT_AGENT,
 
275
            signum,
 
276
            self.child_agent.name if self.child_agent is not None else CURRENT_AGENT)
 
277
        self.child_process.send_signal(signum)
 
278
 
 
279
        if self.signal_handler not in (None, signal.SIG_IGN, signal.SIG_DFL):
 
280
            self.signal_handler(signum, frame)
 
281
        elif self.signal_handler is signal.SIG_DFL:
 
282
            if signum == signal.SIGTERM:
 
283
                # TODO: This should set self.running to False vs. just exiting
 
284
                sys.exit(0)
 
285
        return
 
286
 
 
287
    def get_latest_agent(self):
 
288
        """
 
289
        If autoupdate is enabled, return the most current, downloaded,
 
290
        non-blacklisted agent (if any).
 
291
        Otherwise, return None (implying to use the installed agent).
 
292
        """
 
293
 
 
294
        if not conf.get_autoupdate_enabled():
 
295
            return None
 
296
        
 
297
        self._load_agents()
 
298
        available_agents = [agent for agent in self.agents if agent.is_available]
 
299
        return available_agents[0] if len(available_agents) >= 1 else None
 
300
 
 
301
    def _emit_restart_event(self):
 
302
        if not self._is_clean_start:
 
303
            msg = u"{0} unexpectedly restarted".format(CURRENT_AGENT)
 
304
            logger.info(msg)
 
305
            add_event(
 
306
                AGENT_NAME,
 
307
                version=CURRENT_VERSION,
 
308
                op=WALAEventOperation.Restart,
 
309
                is_success=False,
 
310
                message=msg)
 
311
 
 
312
        self._set_sentinal() 
 
313
        return
 
314
 
 
315
    def _ensure_latest_agent(self, base_version=CURRENT_VERSION):
 
316
        # Ignore new agents if updating is disabled
 
317
        if not conf.get_autoupdate_enabled():
 
318
            return False
 
319
 
 
320
        now = time.time()
 
321
        if self.last_attempt_time is not None:
 
322
            next_attempt_time = self.last_attempt_time + conf.get_autoupdate_frequency()
 
323
        else:
 
324
            next_attempt_time = now
 
325
        if next_attempt_time > now:
 
326
            return False
 
327
 
 
328
        family = conf.get_autoupdate_gafamily()
 
329
        logger.info("Checking for agent family {0} updates", family)
 
330
 
 
331
        self.last_attempt_time = now
 
332
        try:
 
333
            protocol = self.protocol_util.get_protocol()
 
334
            manifest_list, etag = protocol.get_vmagent_manifests()
 
335
        except Exception as e:
 
336
            msg = u"Exception retrieving agent manifests: {0}".format(ustr(e))
 
337
            logger.warn(msg)
 
338
            add_event(
 
339
                AGENT_NAME,
 
340
                op=WALAEventOperation.Download,
 
341
                version=CURRENT_VERSION,
 
342
                is_success=False,
 
343
                message=msg)
 
344
            return False
 
345
 
 
346
        if self.last_etag is not None and self.last_etag == etag:
 
347
            logger.info(u"Incarnation {0} has no agent updates", etag)
 
348
            return False
 
349
 
 
350
        manifests = [m for m in manifest_list.vmAgentManifests if m.family == family]
 
351
        if len(manifests) == 0:
 
352
            logger.info(u"Incarnation {0} has no agent family {1} updates", etag, family)
 
353
            return False
 
354
 
 
355
        try:
 
356
            pkg_list = protocol.get_vmagent_pkgs(manifests[0])
 
357
        except ProtocolError as e:
 
358
            msg= u"Incarnation {0} failed to get {1} package list: {2}".format(
 
359
                etag,
 
360
                family,
 
361
                ustr(e))
 
362
            logger.warn(msg)
 
363
            add_event(
 
364
                AGENT_NAME,
 
365
                op=WALAEventOperation.Download,
 
366
                version=CURRENT_VERSION,
 
367
                is_success=False,
 
368
                message=msg)
 
369
            return False
 
370
 
 
371
        # Set the agents to those available for download at least as current as the existing agent
 
372
        # and remove from disk any agent no longer reported to the VM.
 
373
        # Note:
 
374
        #  The code leaves on disk available, but blacklisted, agents so as to preserve the state.
 
375
        #  Otherwise, those agents could be again downloaded and inappropriately retried.
 
376
        self._set_agents([GuestAgent(pkg=pkg) for pkg in pkg_list.versions])
 
377
        self._purge_agents()
 
378
        self._filter_blacklisted_agents()
 
379
 
 
380
        # Return True if agents more recent than the current are available
 
381
        return len(self.agents) > 0 and self.agents[0].version > base_version
 
382
 
 
383
    def _ensure_no_orphans(self, orphan_wait_interval=ORPHAN_WAIT_INTERVAL):
 
384
        previous_pid_file, pid_file = self._write_pid_file()
 
385
        if previous_pid_file is not None:
 
386
            try:
 
387
                pid = fileutil.read_file(previous_pid_file)
 
388
                wait_interval = orphan_wait_interval
 
389
                while self.osutil.check_pid_alive(pid):
 
390
                    wait_interval -= GOAL_STATE_INTERVAL
 
391
                    if wait_interval <= 0:
 
392
                        logger.warn(
 
393
                            u"{0} forcibly terminated orphan process {1}",
 
394
                            CURRENT_AGENT,
 
395
                            pid)
 
396
                        os.kill(pid, signal.SIGKILL)
 
397
                        break
 
398
                    
 
399
                    logger.info(
 
400
                        u"{0} waiting for orphan process {1} to terminate",
 
401
                        CURRENT_AGENT,
 
402
                        pid)
 
403
                    time.sleep(GOAL_STATE_INTERVAL)
 
404
 
 
405
            except Exception as e:
 
406
                logger.warn(
 
407
                    u"Exception occurred waiting for orphan agent to terminate: {0}",
 
408
                    ustr(e))
 
409
        return
 
410
 
 
411
    def _evaluate_agent_health(self, latest_agent):
 
412
        """
 
413
        Evaluate the health of the selected agent: If it is restarting
 
414
        too frequently, raise an Exception to force blacklisting.
 
415
        """
 
416
        if latest_agent is None:
 
417
            self.child_agent = None
 
418
            return
 
419
 
 
420
        if self.child_agent is None or latest_agent.version != self.child_agent.version:
 
421
            self.child_agent = latest_agent
 
422
            self.child_launch_time = None
 
423
            self.child_launch_attempts = 0
 
424
 
 
425
        if self.child_launch_time is None:
 
426
            self.child_launch_time = time.time()
 
427
 
 
428
        self.child_launch_attempts += 1
 
429
 
 
430
        if (time.time() - self.child_launch_time) <= CHILD_LAUNCH_INTERVAL \
 
431
            and self.child_launch_attempts >= CHILD_LAUNCH_RESTART_MAX:
 
432
                msg = u"Agent {0} restarted more than {1} times in {2} seconds".format(
 
433
                    self.child_agent.name,
 
434
                    CHILD_LAUNCH_RESTART_MAX,
 
435
                    CHILD_LAUNCH_INTERVAL)
 
436
                raise Exception(msg)
 
437
        return
 
438
 
 
439
    def _filter_blacklisted_agents(self):
 
440
        self.agents = [agent for agent in self.agents if not agent.is_blacklisted]
 
441
        return
 
442
 
 
443
    def _get_pid_files(self):
 
444
        pid_file = conf.get_agent_pid_file_path()
 
445
        
 
446
        pid_dir = os.path.dirname(pid_file)
 
447
        pid_name = os.path.basename(pid_file)
 
448
        
 
449
        pid_re = re.compile("(\d+)_{0}".format(re.escape(pid_name)))
 
450
        pid_files = [int(pid_re.match(f).group(1)) for f in os.listdir(pid_dir) if pid_re.match(f)]
 
451
        pid_files.sort()
 
452
 
 
453
        pid_index = -1 if len(pid_files) <= 0 else pid_files[-1]
 
454
        previous_pid_file = None \
 
455
                        if pid_index < 0 \
 
456
                        else os.path.join(pid_dir, "{0}_{1}".format(pid_index, pid_name))
 
457
        pid_file = os.path.join(pid_dir, "{0}_{1}".format(pid_index+1, pid_name))
 
458
        return previous_pid_file, pid_file
 
459
 
 
460
    @property
 
461
    def _is_clean_start(self):
 
462
        if not os.path.isfile(self._sentinal_file_path()):
 
463
            return True
 
464
 
 
465
        try:
 
466
            if fileutil.read_file(self._sentinal_file_path()) != CURRENT_AGENT:
 
467
                return True
 
468
        except Exception as e:
 
469
            logger.warn(
 
470
                u"Exception reading sentinal file {0}: {1}",
 
471
                self._sentinal_file_path(),
 
472
                str(e))
 
473
 
 
474
        return False
 
475
 
 
476
    @property
 
477
    def _is_orphaned(self):
 
478
        parent_pid = os.getppid()
 
479
        if parent_pid in (1, None):
 
480
            return True
 
481
 
 
482
        if not os.path.isfile(conf.get_agent_pid_file_path()):
 
483
            return True
 
484
 
 
485
        return fileutil.read_file(conf.get_agent_pid_file_path()) != ustr(parent_pid)
 
486
 
 
487
    def _load_agents(self):
 
488
        """
 
489
        Load all non-blacklisted agents currently on disk.
 
490
        """
 
491
        if len(self.agents) <= 0:
 
492
            try:
 
493
                path = os.path.join(conf.get_lib_dir(), "{0}-*".format(AGENT_NAME))
 
494
                self._set_agents([GuestAgent(path=agent_dir)
 
495
                                    for agent_dir in glob.iglob(path) if os.path.isdir(agent_dir)])
 
496
                self._filter_blacklisted_agents()
 
497
            except Exception as e:
 
498
                logger.warn(u"Exception occurred loading available agents: {0}", ustr(e))
 
499
        return
 
500
 
 
501
    def _purge_agents(self):
 
502
        """
 
503
        Remove from disk all directories and .zip files of unknown agents
 
504
        (without removing the current, running agent).
 
505
        """
 
506
        path = os.path.join(conf.get_lib_dir(), "{0}-*".format(AGENT_NAME))
 
507
 
 
508
        known_versions = [agent.version for agent in self.agents]
 
509
        if not is_current_agent_installed() and CURRENT_VERSION not in known_versions:
 
510
            logger.warn(
 
511
                u"Running Agent {0} was not found in the agent manifest - adding to list",
 
512
                CURRENT_VERSION)
 
513
            known_versions.append(CURRENT_VERSION)
 
514
 
 
515
        for agent_path in glob.iglob(path):
 
516
            try:
 
517
                name = fileutil.trim_ext(agent_path, "zip")
 
518
                m = AGENT_DIR_PATTERN.match(name)
 
519
                if m is not None and FlexibleVersion(m.group(1)) not in known_versions:
 
520
                    if os.path.isfile(agent_path):
 
521
                        logger.info(u"Purging outdated Agent file {0}", agent_path)
 
522
                        os.remove(agent_path)
 
523
                    else:
 
524
                        logger.info(u"Purging outdated Agent directory {0}", agent_path)
 
525
                        shutil.rmtree(agent_path)
 
526
            except Exception as e:
 
527
                logger.warn(u"Purging {0} raised exception: {1}", agent_path, ustr(e))
 
528
        return
 
529
 
 
530
    def _set_agents(self, agents=[]):
 
531
        self.agents = agents
 
532
        self.agents.sort(key=lambda agent: agent.version, reverse=True)
 
533
        return
 
534
 
 
535
    def _set_sentinal(self, agent=CURRENT_AGENT):
 
536
        try:
 
537
            fileutil.write_file(self._sentinal_file_path(), agent)
 
538
        except Exception as e:
 
539
            logger.warn(
 
540
                u"Exception writing sentinal file {0}: {1}",
 
541
                self._sentinal_file_path(),
 
542
                str(e))
 
543
        return
 
544
 
 
545
    def _sentinal_file_path(self):
 
546
        return os.path.join(conf.get_lib_dir(), AGENT_SENTINAL_FILE)
 
547
 
 
548
    def _shutdown(self):
 
549
        if not os.path.isfile(self._sentinal_file_path()):
 
550
            return
 
551
 
 
552
        try:
 
553
            os.remove(self._sentinal_file_path())
 
554
        except Exception as e:
 
555
            logger.warn(
 
556
                u"Exception removing sentinal file {0}: {1}",
 
557
                self._sentinal_file_path(),
 
558
                str(e))
 
559
        return
 
560
 
 
561
    def _write_pid_file(self):
 
562
        previous_pid_file, pid_file = self._get_pid_files()
 
563
        try:
 
564
            fileutil.write_file(pid_file, ustr(os.getpid()))
 
565
            logger.info(u"{0} running as process {1}", CURRENT_AGENT, ustr(os.getpid()))
 
566
        except Exception as e:
 
567
            pid_file = None
 
568
            logger.warn(
 
569
                u"Expection writing goal state agent {0} pid to {1}: {2}",
 
570
                CURRENT_AGENT,
 
571
                pid_file,
 
572
                ustr(e))
 
573
        return previous_pid_file, pid_file
 
574
 
 
575
 
 
576
class GuestAgent(object):
 
577
    def __init__(self, path=None, pkg=None):
 
578
        self.pkg = pkg
 
579
        version = None
 
580
        if path is not None:
 
581
            m = AGENT_DIR_PATTERN.match(path)
 
582
            if m == None:
 
583
                raise UpdateError(u"Illegal agent directory: {0}".format(path))
 
584
            version = m.group(1)
 
585
        elif self.pkg is not None:
 
586
            version = pkg.version
 
587
 
 
588
        if version == None:
 
589
            raise UpdateError(u"Illegal agent version: {0}".format(version))
 
590
        self.version = FlexibleVersion(version)
 
591
 
 
592
        location = u"disk" if path is not None else u"package"
 
593
        logger.info(u"Instantiating Agent {0} from {1}", self.name, location)
 
594
 
 
595
        self.error = None
 
596
        self._load_error()
 
597
        self._ensure_downloaded()
 
598
        return
 
599
 
 
600
    @property
 
601
    def name(self):
 
602
        return "{0}-{1}".format(AGENT_NAME, self.version)
 
603
 
 
604
    def get_agent_cmd(self):
 
605
        return self.manifest.get_enable_command()
 
606
 
 
607
    def get_agent_dir(self):
 
608
        return os.path.join(conf.get_lib_dir(), self.name)
 
609
 
 
610
    def get_agent_error_file(self):
 
611
        return os.path.join(conf.get_lib_dir(), self.name, AGENT_ERROR_FILE)
 
612
 
 
613
    def get_agent_manifest_path(self):
 
614
        return os.path.join(self.get_agent_dir(), AGENT_MANIFEST_FILE)
 
615
 
 
616
    def get_agent_pkg_path(self):
 
617
        return ".".join((os.path.join(conf.get_lib_dir(), self.name), "zip"))
 
618
 
 
619
    def clear_error(self):
 
620
        self.error.clear()
 
621
        return
 
622
 
 
623
    @property
 
624
    def is_available(self):
 
625
        return self.is_downloaded and not self.is_blacklisted
 
626
 
 
627
    @property
 
628
    def is_blacklisted(self):
 
629
        return self.error is not None and self.error.is_blacklisted
 
630
 
 
631
    @property
 
632
    def is_downloaded(self):
 
633
        return self.is_blacklisted or os.path.isfile(self.get_agent_manifest_path())
 
634
 
 
635
    def mark_failure(self, is_fatal=False):
 
636
        try:
 
637
            if not os.path.isdir(self.get_agent_dir()):
 
638
                os.makedirs(self.get_agent_dir())
 
639
            self.error.mark_failure(is_fatal=is_fatal)
 
640
            self.error.save()
 
641
            if is_fatal:
 
642
                logger.warn(u"Agent {0} is permanently blacklisted", self.name)
 
643
        except Exception as e:
 
644
            logger.warn(u"Agent {0} failed recording error state: {1}", self.name, ustr(e))
 
645
        return
 
646
 
 
647
    def _ensure_downloaded(self):
 
648
        try:
 
649
            logger.info(u"Ensuring Agent {0} is downloaded", self.name)
 
650
 
 
651
            if self.is_blacklisted:
 
652
                logger.info(u"Agent {0} is blacklisted - skipping download", self.name)
 
653
                return
 
654
 
 
655
            if self.is_downloaded:
 
656
                logger.info(u"Agent {0} was previously downloaded - skipping download", self.name)
 
657
                self._load_manifest()
 
658
                return
 
659
 
 
660
            if self.pkg is None:
 
661
                raise UpdateError(u"Agent {0} is missing package and download URIs".format(
 
662
                    self.name))
 
663
            
 
664
            self._download()
 
665
            self._unpack()
 
666
            self._load_manifest()
 
667
            self._load_error()
 
668
 
 
669
            msg = u"Agent {0} downloaded successfully".format(self.name)
 
670
            logger.info(msg)
 
671
            add_event(
 
672
                AGENT_NAME,
 
673
                version=self.version,
 
674
                op=WALAEventOperation.Install,
 
675
                is_success=True,
 
676
                message=msg)
 
677
 
 
678
        except Exception as e:
 
679
            # Note the failure, blacklist the agent if the package downloaded
 
680
            # - An exception with a downloaded package indicates the package
 
681
            #   is corrupt (e.g., missing the HandlerManifest.json file)
 
682
            self.mark_failure(is_fatal=os.path.isfile(self.get_agent_pkg_path()))
 
683
 
 
684
            msg = u"Agent {0} download failed with exception: {1}".format(self.name, ustr(e))
 
685
            logger.warn(msg)
 
686
            add_event(
 
687
                AGENT_NAME,
 
688
                version=self.version,
 
689
                op=WALAEventOperation.Install,
 
690
                is_success=False,
 
691
                message=msg)
 
692
        return
 
693
 
 
694
    def _download(self):
 
695
        package = None
 
696
 
 
697
        for uri in self.pkg.uris:
 
698
            try:
 
699
                resp = restutil.http_get(uri.uri, chk_proxy=True)
 
700
                if resp.status == restutil.httpclient.OK:
 
701
                    package = resp.read()
 
702
                    fileutil.write_file(self.get_agent_pkg_path(), bytearray(package), asbin=True)
 
703
                    logger.info(u"Agent {0} downloaded from {1}", self.name, uri.uri)
 
704
                    break
 
705
            except restutil.HttpError as e:
 
706
                logger.warn(u"Agent {0} download from {1} failed", self.name, uri.uri)
 
707
 
 
708
        if not os.path.isfile(self.get_agent_pkg_path()):
 
709
            msg = u"Unable to download Agent {0} from any URI".format(self.name)
 
710
            add_event(
 
711
                AGENT_NAME,
 
712
                op=WALAEventOperation.Download,
 
713
                version=CURRENT_VERSION,
 
714
                is_success=False,
 
715
                message=msg)
 
716
            raise UpdateError(msg)
 
717
        return
 
718
 
 
719
    def _load_error(self):
 
720
        try:
 
721
            if self.error is None:
 
722
                self.error = GuestAgentError(self.get_agent_error_file())
 
723
            self.error.load()
 
724
            logger.info(u"Agent {0} error state: {1}", self.name, ustr(self.error))
 
725
        except Exception as e:
 
726
            logger.warn(u"Agent {0} failed loading error state: {1}", self.name, ustr(e))
 
727
        return
 
728
 
 
729
    def _load_manifest(self):
 
730
        path = self.get_agent_manifest_path()
 
731
        if not os.path.isfile(path):
 
732
            msg = u"Agent {0} is missing the {1} file".format(self.name, AGENT_MANIFEST_FILE)
 
733
            raise UpdateError(msg)
 
734
 
 
735
        with open(path, "r") as manifest_file:
 
736
            try:
 
737
                manifests = json.load(manifest_file)
 
738
            except Exception as e:
 
739
                msg = u"Agent {0} has a malformed {1}".format(self.name, AGENT_MANIFEST_FILE)
 
740
                raise UpdateError(msg)
 
741
            if type(manifests) is list:
 
742
                if len(manifests) <= 0:
 
743
                    msg = u"Agent {0} has an empty {1}".format(self.name, AGENT_MANIFEST_FILE)
 
744
                    raise UpdateError(msg)
 
745
                manifest = manifests[0]
 
746
            else:
 
747
                manifest = manifests
 
748
 
 
749
        try:
 
750
            self.manifest = HandlerManifest(manifest)
 
751
            if len(self.manifest.get_enable_command()) <= 0:
 
752
                raise Exception(u"Manifest is missing the enable command")
 
753
        except Exception as e:
 
754
            msg = u"Agent {0} has an illegal {1}: {2}".format(
 
755
                self.name,
 
756
                AGENT_MANIFEST_FILE,
 
757
                ustr(e))
 
758
            raise UpdateError(msg)
 
759
 
 
760
        logger.info(
 
761
            u"Agent {0} loaded manifest from {1}",
 
762
            self.name,
 
763
            self.get_agent_manifest_path())
 
764
        logger.verbose(u"Successfully loaded Agent {0} {1}: {2}",
 
765
            self.name,
 
766
            AGENT_MANIFEST_FILE,
 
767
            ustr(self.manifest.data))
 
768
        return
 
769
 
 
770
    def _unpack(self):
 
771
        try:
 
772
            if os.path.isdir(self.get_agent_dir()):
 
773
                shutil.rmtree(self.get_agent_dir())
 
774
 
 
775
            zipfile.ZipFile(self.get_agent_pkg_path()).extractall(self.get_agent_dir())
 
776
 
 
777
        except Exception as e:
 
778
            msg = u"Exception unpacking Agent {0} from {1}: {2}".format(
 
779
                self.name,
 
780
                self.get_agent_pkg_path(),
 
781
                ustr(e))
 
782
            raise UpdateError(msg)
 
783
 
 
784
        if not os.path.isdir(self.get_agent_dir()):
 
785
            msg = u"Unpacking Agent {0} failed to create directory {1}".format(
 
786
                self.name,
 
787
                self.get_agent_dir())
 
788
            raise UpdateError(msg)
 
789
 
 
790
        logger.info(
 
791
            u"Agent {0} unpacked successfully to {1}",
 
792
            self.name,
 
793
            self.get_agent_dir())
 
794
        return
 
795
 
 
796
 
 
797
class GuestAgentError(object):
 
798
    def __init__(self, path):
 
799
        if path is None:
 
800
            raise UpdateError(u"GuestAgentError requires a path")
 
801
        self.path = path
 
802
 
 
803
        self.clear()
 
804
        self.load()
 
805
        return
 
806
   
 
807
    def mark_failure(self, is_fatal=False):
 
808
        self.last_failure = time.time()
 
809
        self.failure_count += 1
 
810
        self.was_fatal = is_fatal
 
811
        return
 
812
 
 
813
    def clear(self):
 
814
        self.last_failure = 0.0
 
815
        self.failure_count = 0
 
816
        self.was_fatal = False
 
817
        return
 
818
    
 
819
    def clear_old_failure(self):
 
820
        if self.last_failure <= 0.0:
 
821
            return
 
822
        if self.last_failure < (time.time() - RETAIN_INTERVAL):
 
823
            self.clear()
 
824
        return
 
825
 
 
826
    @property
 
827
    def is_blacklisted(self):
 
828
        return self.was_fatal or self.failure_count >= MAX_FAILURE
 
829
 
 
830
    def load(self):
 
831
        if self.path is not None and os.path.isfile(self.path):
 
832
            with open(self.path, 'r') as f:
 
833
                self.from_json(json.load(f))
 
834
        return
 
835
 
 
836
    def save(self):
 
837
        if os.path.isdir(os.path.dirname(self.path)):
 
838
            with open(self.path, 'w') as f:
 
839
                json.dump(self.to_json(), f)
 
840
        return
 
841
    
 
842
    def from_json(self, data):
 
843
        self.last_failure = max(
 
844
            self.last_failure,
 
845
            data.get(u"last_failure", 0.0))
 
846
        self.failure_count = max(
 
847
            self.failure_count,
 
848
            data.get(u"failure_count", 0))
 
849
        self.was_fatal = self.was_fatal or data.get(u"was_fatal", False)
 
850
        return
 
851
 
 
852
    def to_json(self):
 
853
        data = {
 
854
            u"last_failure": self.last_failure,
 
855
            u"failure_count": self.failure_count,
 
856
            u"was_fatal" : self.was_fatal
 
857
        }  
 
858
        return data
 
859
 
 
860
    def __str__(self):
 
861
        return "Last Failure: {0}, Total Failures: {1}, Fatal: {2}".format(
 
862
            self.last_failure,
 
863
            self.failure_count,
 
864
            self.was_fatal)