WalletMgr.py 12.0 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14
import subprocess
import time
import shutil
import signal
import os
from collections import namedtuple
import re
import sys

from testUtils import Utils

Wallet=namedtuple("Wallet", "name password host port")
# pylint: disable=too-many-instance-attributes
class WalletMgr(object):
15 16
    __walletLogOutFile="test_keosd_out.log"
    __walletLogErrFile="test_keosd_err.log"
17
    __walletDataDir="test_wallet_0"
18
    __MaxPort=9999
19 20 21

    # pylint: disable=too-many-arguments
    # walletd [True|False] True=Launch wallet(keosd) process; False=Manage launch process externally.
22
    def __init__(self, walletd, nodeosPort=8888, nodeosHost="localhost", port=9899, host="localhost"):
23 24 25 26 27 28 29
        self.walletd=walletd
        self.nodeosPort=nodeosPort
        self.nodeosHost=nodeosHost
        self.port=port
        self.host=host
        self.wallets={}
        self.__walletPid=None
30 31

    def getWalletEndpointArgs(self):
32
        if not self.walletd or not self.isLaunched():
33 34 35 36
            return ""

        return " --wallet-url http://%s:%d" % (self.host, self.port)

37 38
    def getArgs(self):
        return " --url http://%s:%d%s %s" % (self.nodeosHost, self.nodeosPort, self.getWalletEndpointArgs(), Utils.MiscEosClientArgs)
39 40 41

    def isLaunched(self):
        return self.__walletPid is not None
42

43 44 45 46 47 48 49 50 51 52 53 54 55 56
    def isLocal(self):
        return self.host=="localhost" or self.host=="127.0.0.1"

    def findAvailablePort(self):
        for i in range(WalletMgr.__MaxPort):
            port=self.port+i
            if port > WalletMgr.__MaxPort:
                port-=WalletMgr.__MaxPort
            if Utils.arePortsAvailable(port):
                return port
            if Utils.Debug: Utils.Print("Port %d not available for %s" % (port, Utils.EosWalletPath))

        Utils.errorExit("Failed to find free port to use for %s" % (Utils.EosWalletPath))

57 58 59 60 61
    def launch(self):
        if not self.walletd:
            Utils.Print("ERROR: Wallet Manager wasn't configured to launch keosd")
            return False

62 63 64
        if self.isLaunched():
            return True

65 66 67
        if self.isLocal():
            self.port=self.findAvailablePort()

68
        pgrepCmd=Utils.pgrepCmd(Utils.EosWalletName)
69 70
        if Utils.Debug:
            portTaken=False
71
            if self.isLocal():
72
                if not Utils.arePortsAvailable(self.port):
73
                    portTaken=True
74 75 76 77 78 79 80 81 82
            psOut=Utils.checkOutput(pgrepCmd.split(), ignoreError=True)
            if psOut or portTaken:
                statusMsg=""
                if psOut:
                    statusMsg+=" %s - {%s}." % (pgrepCmd, psOut)
                if portTaken:
                    statusMsg+=" port %d is NOT available." % (self.port)
                Utils.Print("Launching %s, note similar processes running. %s" % (Utils.EosWalletName, statusMsg))

83 84 85
        cmd="%s --data-dir %s --config-dir %s --http-server-address=%s:%d --verbose-http-errors" % (
            Utils.EosWalletPath, WalletMgr.__walletDataDir, WalletMgr.__walletDataDir, self.host, self.port)
        if Utils.Debug: Utils.Print("cmd: %s" % (cmd))
86
        with open(WalletMgr.__walletLogOutFile, 'w') as sout, open(WalletMgr.__walletLogErrFile, 'w') as serr:
87 88 89 90
            popen=subprocess.Popen(cmd.split(), stdout=sout, stderr=serr)
            self.__walletPid=popen.pid

        # Give keosd time to warm up
91
        time.sleep(2)
92

93
        try:
94
            if Utils.Debug: Utils.Print("Checking if %s launched. %s" % (Utils.EosWalletName, pgrepCmd))
95
            psOut=Utils.checkOutput(pgrepCmd.split())
96
            if Utils.Debug: Utils.Print("Launched %s. {%s}" % (Utils.EosWalletName, psOut))
97
        except subprocess.CalledProcessError as ex:
98
            Utils.errorExit("Failed to launch the wallet manager")
99

100 101
        return True

102
    def create(self, name, accounts=None, exitOnError=True):
103 104 105 106 107
        wallet=self.wallets.get(name)
        if wallet is not None:
            if Utils.Debug: Utils.Print("Wallet \"%s\" already exists. Returning same." % name)
            return wallet
        p = re.compile(r'\n\"(\w+)\"\n', re.MULTILINE)
108
        cmdDesc="wallet create"
109
        cmd="%s %s %s --name %s --to-console" % (Utils.EosClientPath, self.getArgs(), cmdDesc, name)
110
        if Utils.Debug: Utils.Print("cmd: %s" % (cmd))
111
        retStr=None
112 113 114 115 116 117 118 119 120 121
        maxRetryCount=4
        retryCount=0
        while True:
            try:
                retStr=Utils.checkOutput(cmd.split())
                break
            except subprocess.CalledProcessError as ex:
                retryCount+=1
                if retryCount<maxRetryCount:
                    delay=10
122 123 124
                    pgrepCmd=Utils.pgrepCmd(Utils.EosWalletName)
                    psOut=Utils.checkOutput(pgrepCmd.split())
                    portStatus="N/A"
125
                    if self.isLocal():
126 127 128 129 130
                        if Utils.arePortsAvailable(self.port):
                            portStatus="AVAILABLE"
                        else:
                            portStatus="NOT AVAILABLE"
                    if Utils.Debug: Utils.Print("%s was not accepted, delaying for %d seconds and trying again. port %d is %s. %s - {%s}" % (cmdDesc, delay, self.port, pgrepCmd, psOut))
131 132 133 134
                    time.sleep(delay)
                    continue

                msg=ex.output.decode("utf-8")
135
                errorMsg="ERROR: Failed to create wallet - %s. %s" % (name, msg)
136
                if exitOnError:
137 138
                    Utils.errorExit("%s" % (errorMsg))
                Utils.Print("%s" % (errorMsg))
139
                return None
140

141 142
        m=p.search(retStr)
        if m is None:
143 144
            if exitOnError:
                Utils.cmdError("could not create wallet %s" % (name))
145
                Utils.errorExit("Failed  to create wallet %s" % (name))
146

147 148 149 150 151 152 153
            Utils.Print("ERROR: wallet password parser failure")
            return None
        p=m.group(1)
        wallet=Wallet(name, p, self.host, self.port)
        self.wallets[name] = wallet

        if accounts:
154
            self.importKeys(accounts,wallet)
155 156 157

        return wallet

158 159 160 161 162 163 164 165
    def importKeys(self, accounts, wallet, ignoreDupKeyWarning=False):
        for account in accounts:
            Utils.Print("Importing keys for account %s into wallet %s." % (account.name, wallet.name))
            if not self.importKey(account, wallet, ignoreDupKeyWarning):
                Utils.Print("ERROR: Failed to import key for account %s" % (account.name))
                return False

    def importKey(self, account, wallet, ignoreDupKeyWarning=False):
166
        warningMsg="Key already in wallet"
167
        cmd="%s %s wallet import --name %s --private-key %s" % (
168
            Utils.EosClientPath, self.getArgs(), wallet.name, account.ownerPrivateKey)
169 170 171 172 173 174
        if Utils.Debug: Utils.Print("cmd: %s" % (cmd))
        try:
            Utils.checkOutput(cmd.split())
        except subprocess.CalledProcessError as ex:
            msg=ex.output.decode("utf-8")
            if warningMsg in msg:
175 176
                if not ignoreDupKeyWarning:
                    Utils.Print("WARNING: This key is already imported into the wallet.")
177 178 179 180 181 182 183
            else:
                Utils.Print("ERROR: Failed to import account owner key %s. %s" % (account.ownerPrivateKey, msg))
                return False

        if account.activePrivateKey is None:
            Utils.Print("WARNING: Active private key is not defined for account \"%s\"" % (account.name))
        else:
184
            cmd="%s %s wallet import --name %s  --private-key %s" % (
185
                Utils.EosClientPath, self.getArgs(), wallet.name, account.activePrivateKey)
186 187 188 189 190 191
            if Utils.Debug: Utils.Print("cmd: %s" % (cmd))
            try:
                Utils.checkOutput(cmd.split())
            except subprocess.CalledProcessError as ex:
                msg=ex.output.decode("utf-8")
                if warningMsg in msg:
192 193
                    if not ignoreDupKeyWarning:
                        Utils.Print("WARNING: This key is already imported into the wallet.")
194 195 196 197 198 199 200 201
                else:
                    Utils.Print("ERROR: Failed to import account active key %s. %s" %
                                (account.activePrivateKey, msg))
                    return False

        return True

    def lockWallet(self, wallet):
202
        cmd="%s %s wallet lock --name %s" % (Utils.EosClientPath, self.getArgs(), wallet.name)
203 204 205 206 207 208 209 210
        if Utils.Debug: Utils.Print("cmd: %s" % (cmd))
        if 0 != subprocess.call(cmd.split(), stdout=Utils.FNull):
            Utils.Print("ERROR: Failed to lock wallet %s." % (wallet.name))
            return False

        return True

    def unlockWallet(self, wallet):
211
        cmd="%s %s wallet unlock --name %s" % (Utils.EosClientPath, self.getArgs(), wallet.name)
212 213 214 215 216 217 218 219 220 221
        if Utils.Debug: Utils.Print("cmd: %s" % (cmd))
        popen=subprocess.Popen(cmd.split(), stdout=Utils.FNull, stdin=subprocess.PIPE)
        _, errs = popen.communicate(input=wallet.password.encode("utf-8"))
        if 0 != popen.wait():
            Utils.Print("ERROR: Failed to unlock wallet %s: %s" % (wallet.name, errs.decode("utf-8")))
            return False

        return True

    def lockAllWallets(self):
222
        cmd="%s %s wallet lock_all" % (Utils.EosClientPath, self.getArgs())
223 224 225 226 227 228 229 230 231 232 233
        if Utils.Debug: Utils.Print("cmd: %s" % (cmd))
        if 0 != subprocess.call(cmd.split(), stdout=Utils.FNull):
            Utils.Print("ERROR: Failed to lock all wallets.")
            return False

        return True

    def getOpenWallets(self):
        wallets=[]

        p = re.compile(r'\s+\"(\w+)\s\*\",?\n', re.MULTILINE)
234
        cmd="%s %s wallet list" % (Utils.EosClientPath, self.getArgs())
235
        if Utils.Debug: Utils.Print("cmd: %s" % (cmd))
236 237 238 239 240 241 242 243
        retStr=None
        try:
            retStr=Utils.checkOutput(cmd.split())
        except subprocess.CalledProcessError as ex:
            msg=ex.output.decode("utf-8")
            Utils.Print("ERROR: Failed to open wallets. %s" % (msg))
            return False

244 245 246 247 248 249 250 251 252 253 254 255
        m=p.findall(retStr)
        if m is None:
            Utils.Print("ERROR: wallet list parser failure")
            return None
        wallets=m

        return wallets

    def getKeys(self, wallet):
        keys=[]

        p = re.compile(r'\n\s+\"(\w+)\"\n', re.MULTILINE)
256
        cmd="%s %s wallet private_keys --name %s --password %s " % (Utils.EosClientPath, self.getArgs(), wallet.name, wallet.password)
257
        if Utils.Debug: Utils.Print("cmd: %s" % (cmd))
258 259 260 261 262 263 264
        retStr=None
        try:
            retStr=Utils.checkOutput(cmd.split())
        except subprocess.CalledProcessError as ex:
            msg=ex.output.decode("utf-8")
            Utils.Print("ERROR: Failed to get keys. %s" % (msg))
            return False
265 266 267 268 269 270 271 272 273 274 275 276
        m=p.findall(retStr)
        if m is None:
            Utils.Print("ERROR: wallet private_keys parser failure")
            return None
        keys=m

        return keys


    def dumpErrorDetails(self):
        Utils.Print("=================================================================")
        if self.__walletPid is not None:
277 278 279 280 281
            Utils.Print("Contents of %s:" % (WalletMgr.__walletLogOutFile))
            Utils.Print("=================================================================")
            with open(WalletMgr.__walletLogOutFile, "r") as f:
                shutil.copyfileobj(f, sys.stdout)
            Utils.Print("Contents of %s:" % (WalletMgr.__walletLogErrFile))
282
            Utils.Print("=================================================================")
283
            with open(WalletMgr.__walletLogErrFile, "r") as f:
284 285 286 287 288
                shutil.copyfileobj(f, sys.stdout)

    def killall(self, allInstances=False):
        """Kill keos instances. allInstances will kill all keos instances running on the system."""
        if self.__walletPid:
289
            Utils.Print("Killing wallet manager process %d" % (self.__walletPid))
290 291 292 293 294 295 296 297 298 299 300 301 302
            os.kill(self.__walletPid, signal.SIGKILL)

        if allInstances:
            cmd="pkill -9 %s" % (Utils.EosWalletName)
            if Utils.Debug: Utils.Print("cmd: %s" % (cmd))
            subprocess.call(cmd.split())


    @staticmethod
    def cleanup():
        dataDir=WalletMgr.__walletDataDir
        if os.path.isdir(dataDir) and os.path.exists(dataDir):
            shutil.rmtree(WalletMgr.__walletDataDir)