Passed
Push — develop ( a04425...8843c6 )
by Nikolay
05:16 queued 14s
created

YandexCloud   A

Complexity

Total Complexity 14

Size/Duplication

Total Lines 109
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 14
eloc 40
c 2
b 0
f 0
dl 0
loc 109
rs 10

5 Methods

Rating   Name   Duplication   Size   Complexity  
A notContainsGoogleDomain() 0 10 4
A extractUserNameFromUserData() 0 6 2
A retrieveInstanceMetadata() 0 22 4
A __construct() 0 3 1
A provision() 0 31 3
1
<?php
2
/*
3
 * MikoPBX - free phone system for small business
4
 * Copyright © 2017-2024 Alexey Portnov and Nikolay Beketov
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License along with this program.
17
 * If not, see <https://www.gnu.org/licenses/>.
18
 */
19
20
namespace MikoPBX\Core\System\CloudProvisioning;
21
22
use GuzzleHttp\Client;
23
use GuzzleHttp\Exception\GuzzleException;
24
use MikoPBX\Core\System\SystemMessages;
25
26
class YandexCloud extends CloudProvider
27
{
28
    public const CloudID = 'YandexCloud';
29
30
    private Client $client;
31
32
    public function __construct()
33
    {
34
        $this->client = new Client(['timeout' => self::HTTP_TIMEOUT]);
35
    }
36
37
    /**
38
     * Performs the Yandex Cloud provisioning.
39
     *
40
     * @return bool True if the provisioning was successful, false otherwise.
41
     */
42
    public function provision(): bool
43
    {
44
        $metadata = $this->retrieveInstanceMetadata();
45
        if ($metadata === null || empty($metadata['id'])) {
46
            // If metadata is null ot machine id is unknown, do not proceed with provisioning.
47
            return false;
48
        }
49
50
        SystemMessages::echoToTeletype(PHP_EOL);
51
52
        // Extract username from user-data
53
        $userData = $data['attributes']['user-data'] ?? '';
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $data seems to never exist and therefore isset should always be false.
Loading history...
54
        $username = $this->extractUserNameFromUserData($userData);
55
56
        // Update SSH keys, if available
57
        $this->updateSSHKeys($metadata['attributes']['ssh-keys'] ?? '');
58
59
        // Update machine name
60
        $hostname = $metadata['name'] ?? '';
61
        $this->updateHostName($hostname);
62
63
        // Update LAN settings with the external IP address
64
        $extIp = $metadata['networkInterfaces'][0]['accessConfigs'][0]['externalIp'] ?? '';
65
        $this->updateLanSettings($extIp);
66
67
        // Update SSH and WEB passwords using some unique identifier from the metadata
68
        $vmId = $metadata['id'];
69
        $this->updateSSHCredentials($username ?? 'yc-user', $vmId);
70
        $this->updateWebPassword($vmId);
71
72
        return true;
73
    }
74
75
    /**
76
     * Retrieves Yandex instance metadata.
77
     *
78
     * @return array|null The instance metadata or null if retrieval failed.
79
     *
80
     */
81
    private function retrieveInstanceMetadata(): ?array
82
    {
83
        try {
84
            $response = $this->client->request('GET', 'http://169.254.169.254/computeMetadata/v1/instance/', [
85
                'query' => ['recursive' => 'true', 'alt' => 'json'],
86
                'headers' => ['Metadata-Flavor' => 'Google']
87
            ]);
88
89
            if ($response->getStatusCode() == 200) {
90
                $metadata = json_decode($response->getBody()->getContents(), true);
91
                // Verify that metadata not contains the 'serviceAccounts' with 'gserviceaccount' in any value
92
                if ($this->notContainsGoogleDomain($metadata)) {
93
                    return $metadata;
94
                } else {
95
                    SystemMessages::sysLogMsg(__CLASS__, "Metadata contain 'gserviceaccount' in service account.");
96
                }
97
            }
98
        } catch (GuzzleException $e) {
99
            SystemMessages::sysLogMsg(__CLASS__, "Failed to retrieve Yandex Cloud instance metadata: " . $e->getMessage());
100
        }
101
102
        return null;
103
    }
104
105
    /**
106
     * Extracts the username from user-data script.
107
     *
108
     * @param string $userData Cloud-init user data string.
109
     * @return string|null Extracted username or null if not found.
110
     */
111
    private function extractUserNameFromUserData(string $userData): ?string
112
    {
113
        if (preg_match('/^users:\s*-\s*name:\s*(\w+)/m', $userData, $matches)) {
114
            return $matches[1]; // Returns the first username found
115
        }
116
        return 'null'; // Return null if no username found
117
    }
118
119
    /**
120
     * Checks if the 'serviceAccounts' array contains the word 'gserviceaccount' in any value.
121
     *
122
     * @param array $metadata Metadata array from Google Cloud instance.
123
     * @return bool False if 'gserviceaccount' is found in any service account, true otherwise.
124
     */
125
    private function notContainsGoogleDomain(array $metadata): bool
126
    {
127
        if (isset($metadata['serviceAccounts'])) {
128
            foreach ($metadata['serviceAccounts'] as $account) {
129
                if (strpos($account['email'], 'gserviceaccount') !== false) {
130
                    return false;
131
                }
132
            }
133
        }
134
        return true;
135
    }
136
}